error:'.$e->getMessage().'
'; + $mail_content .= 'url:'.request()->url(true).'
'; + $mail_content .= 'ip:'.request()->ip().'
'; + $mail_content .= '详情请登录宝塔查看日志
'; + + $emails = SundryConfig::val('admin_error_log_email'); + if(config('sys_env') == 'PROD'){//线上环境才发错误邮件 + EmailTool::email_to_person($title,$mail_content,$emails); + } + + if(request()->LogObj){ + request()->LogObj->flush(); + } + if(request()->ServeLogObj){ + request()->ServeLogObj->flush(); + } + + //可以在此交由系统处理 + return parent::render($e); + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/exception/api/ApiException.php b/admin/vendor/wanghua/general-utility-tools-php/src/exception/api/ApiException.php new file mode 100644 index 0000000..f2b795c --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/exception/api/ApiException.php @@ -0,0 +1,21 @@ +where('name',$menu_name)->data(['status'=>'normal'])->update(); + }else{ + Db::table('fa_auth_rule')->where('name',$menu_name)->data(['status'=>'hidden'])->update(); + } + } + return true; + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/file/File.php b/admin/vendor/wanghua/general-utility-tools-php/src/file/File.php new file mode 100644 index 0000000..3e45098 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/file/File.php @@ -0,0 +1,236 @@ + array(1) { + // ["path"] => string(25) "D:\wanghua\test_files/111" + // } + // [1] => array(1) { + // ["path"] => string(25) "D:\wanghua\test_files/222" + // } + //} + * + * + * + * + * @param $file_name_arr 存储文件路径、文件名称 + * + //array(64) { + // [0] => array(3) { + // ["path"] => string(25) "D:\wanghua\test_files/111" + // ["path_file"] => string(33) "D:\wanghua\test_files/111/111.txt" + // ["filename"] => string(7) "111.txt" + // } + // [1] => array(3) { + // ["path"] => string(25) "D:\wanghua\test_files/111" + // ["path_file"] => string(37) "D:\wanghua\test_files/111/1111111.txt" + // ["filename"] => string(11) "1111111.txt" + // } + //} + */ + function read_folder($folderPath,&$dirs,&$file_name_arr) { + // 获取指定目录下所有文件和文件夹 + $files = scandir($folderPath); + foreach ($files as $file) { + // 如果当前项为文件夹且不是"."或者".." + if (is_dir("$folderPath/$file") && !in_array($file, array('.', '..'))) { + $dirs[] = ['path'=>$folderPath.'/'.$file];//此时的$file是文件夹名称 + // 调用自身进行递归操作 + $this->read_folder("$folderPath/$file",$dirs,$file_name_arr); + } elseif (!in_array($file, array('.', '..'))){ // 如果当前项为文件而非"."或者".." + $file_name_arr[] = ['path'=>$folderPath,'path_file'=>"$folderPath/$file",'filename'=>$file]; + } + } + } + + /** + * desc:复制文件 + * + * author:wh + * @param array $sourceFileArr 源文件路径数组 path,path_file,filename + * + * @param string $destination_path 目标存放路径 D:\wanghua\test_files\new_files + * + * @param array $cp_type_arr 允许复制的文件后缀 eg:['jpg','png,'spine'] + * + * @param bool $is_cover 是否覆盖 默认false,不覆盖会以源文件名+微秒时间戳格式作为新的文件名 + * + * @param bool $is_continue 相同文件是否跳过复制 默认 true + * + * 实战案例: + * + function testcopy(){ + $source_dir = 'D:\wanghua\test_files';//源文件路径 + $destination_path = 'D:\wanghua\test_files\new_files';//目标文件路径 + + $dirs = [];//存储文件路径 + $file_name_arr = [];//含文件路径、名称 + $file_obj = new File(); + $file_obj->read_folder($source_dir,$dirs,$file_name_arr); + //dump($dirs); + //dump($file_name_arr);die; + $file_obj->copy($file_name_arr,$destination_path,['jpg','png,'spine']); + } + * + */ + function copy(array $sourceFileArr, string $destination_path, $cp_type_arr=['jpg','png'], $is_cover=false, $is_continue=false){ + //设置不超时 + set_time_limit(0); + //源文件路径 => 目标文件路径 + foreach ($sourceFileArr as $key=>$file_arr){ + if(!file_exists($file_arr['path_file'])){ + continue; + } + //验证目标文件夹路径 + if(!file_exists($destination_path)){ + mkdir($destination_path,0777,true); + } + $name_arr = explode('.',$file_arr['filename']); + //验证符合条件的文件类型 + if(!in_array($name_arr[1],$cp_type_arr)){ + continue; + } + if(file_exists($destination_path.'/'.$file_arr['filename'])){ + if($is_continue){ + continue;//相同文件跳过复制 + } + //覆盖 + if($is_cover){ + copy($file_arr['path_file'], $destination_path.'/'.$file_arr['filename']); + }else{ + //不覆盖,名称区分 + $microsecond = Tools::getMicrosecond(); + copy($file_arr['path_file'], $destination_path.'/'.$name_arr[0].'('.$microsecond.').'.$name_arr[1]); + } + }else{ + copy($file_arr['path_file'], $destination_path.'/'.$file_arr['filename']); + } + } + } + + /** + * desc:递归删除目录 + * + * $dir 物理路径 + * + * author:wh + */ + function recursive_delete($dir) { + if (!is_dir($dir)) { + return unlink($dir); + } + + $handle = opendir($dir); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $path = $dir . DIRECTORY_SEPARATOR . $file; + if (is_dir($path)) { + $this->recursive_delete($path); + } else { + unlink($path); + } + } + closedir($handle); + rmdir($dir); + } + + + /** + * desc:解压ZIP文件并存储在本地 + * author:wh + * @param resource $file_stream 文件流 + * 例如:$response = $client->request('POST', $url, [ + 'headers' => $headers, + 'body' => $body, + ]); + // 处理响应 + //$file_stream = $response->getBody(); + * @param string $save_path 存储路径 + * @return array + * @throws \Exception + */ + function unzip($file_stream,$save_path){ + $zpi_file_name = 'zip_tmp_file_'.Tools::rand_str(20).'.zip'; + //这个目录将会在结束时删除 + $zipTempPath = $save_path.'/tmp_zip_files/'; // 临时存放ZIP文件的路径 + //压缩文件保存目录 + if(!file_exists($zipTempPath)){ + mkdir($zipTempPath,0777,true); + } + $zpi_file = $zipTempPath.$zpi_file_name; + // 保存ZIP文件到临时路径 + file_put_contents($zpi_file, $file_stream); + + $this->unzip_path = $zipTempPath.'unzip_files/'; + //解压目录 + if(!file_exists( $this->unzip_path)){ + mkdir( $this->unzip_path,0777,true); + } + // 解压ZIP文件 + $zip = new \ZipArchive(); + if ($zip->open($zpi_file) === TRUE) { + $zip->extractTo( $this->unzip_path);//移动到临时解压目录 + $zip->close(); + //遍历所有文件(file_ext后缀自行指定),遍历部分文件,如:*.json,*.zip,*.mp4,*.mp3等 + $wavFiles = $this->glob_by_ext( $this->unzip_path.$this->file_ext); + + //$urls = []; + //// 遍历解压后的文件,生成URL + //foreach ($wavFiles as $wavFilePath) { + // // 生成外部访问的URL + // $downloadUrl = request()->domain() . explode('public',$wavFilePath)[1]; + // $urls[] = $downloadUrl; + //} + //// 返回所有WAV文件的URL + //return ['wav_urls' => $urls]; + return $wavFiles; + } else { + throw new \Exception('Failed to open ZIP file.'); + } + } + + /** + * desc:获取指定扩展类型的文件列表 + * + * author:wh + * @param string $file_ext 如: wav,pm4,mp3,json,zip + * @return array|false + */ + function glob_by_ext($file_ext='*'){ + return glob( $this->unzip_path.'*.'.$file_ext); + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/file/README.MD b/admin/vendor/wanghua/general-utility-tools-php/src/file/README.MD new file mode 100644 index 0000000..8459022 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/file/README.MD @@ -0,0 +1,5 @@ +## 文件处理 + +#### 文件夹读取,文件复制 + +#### 文件上传 \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/file/upload/FileUpload.php b/admin/vendor/wanghua/general-utility-tools-php/src/file/upload/FileUpload.php new file mode 100644 index 0000000..961c665 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/file/upload/FileUpload.php @@ -0,0 +1,767 @@ +file_dir = $file_dir; + } + /** + * desc:单图 + * + * 【!!!必须确认php.ini中upload_max_filesize的配置足够大,否则上传失败但不会报错!!!】 + * + * author:wh + * @param string $file_upload_name 表单文件元素name值 + * @param int $size 大小,默认2M + * @param string $ext 允许的扩展 + * @return array + */ + function image($file_upload_name = 'file_upload', $size=10, $ext=''){ + if(!$this->uploadMaxFilesizeCheck($size)){ + return Tools::set_res(1, '上传文件受限'); + } + // 获取表单上传文件 例如上传了001.jpg + $file = request()->file($file_upload_name); + if(empty($file)){ + return Tools::set_res(1, '请上传文件'); + } + // 移动到框架应用根目录/uploads/ 目录下 + $outer_req_url = $this->file_dir.'/image'; + $physics_path = 'public'.$outer_req_url; + $root_path = explode('vendor',__DIR__)[0]; + $upload_dir = $root_path.$physics_path; + if(!is_dir($upload_dir)){ + mkdir($upload_dir, 0777, true); + } + if(empty($file)){ + return Tools::set_res(1, '上传文件不存在'); + } + + if(empty($ext)) $ext = $this->img_ext; + + $info = $file->validate(['size'=>$size * 1024 * 1024, 'ext'=>$ext])->move($upload_dir); + if($info){ + //原文件名 + $source_file_name = $info->getInfo('name'); + + //文件类型 + $file_type = $info->getInfo('type'); + + // 输出扩展名 jpg + $extension = $info->getExtension(); + + // 输出 20160820/42a79759f284b767dfcb2a0197904287.jpg + $save_name = $info->getSaveName(); + + // 输出 42a79759f284b767dfcb2a0197904287.jpg + $new_filename = $info->getFilename(); + + $relative_url = $outer_req_url.'/'.str_replace('\\', '/', $save_name); + $result = [ + 'prefix'=>request()->domain().$outer_req_url,//访问前缀 + 'source_file_name'=>$source_file_name, + 'file_type'=>$file_type, + 'extension'=>$extension, + 'save_name'=>str_replace('\\', '/', $save_name), + 'new_filename'=>$new_filename, + 'real_path'=>$this->dealPath($root_path.'public'.$relative_url),//物理路径 + 'outer_req_url'=>request()->domain().$relative_url, + 'outer_req_relative_url'=>$relative_url, + 'size'=>$info->getSize(), + ]; + return Tools::set_res(0, '上传成功', $result); + }else{ + // 上传失败获取错误信息 + return Tools::set_res(1, $file->getError()); + } + } + + /** + * desc:多图 + * + * 【!!!必须确认php.ini中upload_max_filesize的配置足够大,否则上传失败但不会报错!!!】 + * + * author:wh + * @param string $file_upload_name 表单文件元素name值 + * @param int $size 大小,默认2M + * @param string $ext 允许的扩展 + * @return array + */ + function images($file_upload_name = 'file_upload', $size=10, $ext=''){ + if(!$this->uploadMaxFilesizeCheck($size)){ + return Tools::set_res(1, '上传文件受限'); + } + // 获取表单上传文件 + $files = request()->file($file_upload_name); + if(empty($files)){ + return Tools::set_res(1, '请上传文件'); + } + $outer_req_url = $this->file_dir.'/image'; + $physics_path = 'public'.$outer_req_url; + $root_path = explode('vendor',__DIR__)[0]; + $upload_dir = $root_path.$physics_path; + if(!is_dir($upload_dir)){ + mkdir($upload_dir, 0777, true); + } + + if(empty($ext)) $ext = $this->img_ext; + + $result = []; + $error = []; + foreach($files as $file){ + if(!is_object($file)){ + return Tools::set_res(1, '请上传正确的文件'); + } + // 移动到框架应用根目录/uploads/ 目录下 + $info = $file->validate(['size'=>$size * 1024 * 1024, 'ext'=>$ext])->move($upload_dir); + if($info){ + //原文件名 + $source_file_name = $info->getInfo('name'); + + //文件类型 + $file_type = $info->getInfo('type'); + + // 输出扩展名 jpg + $extension = $info->getExtension(); + + // 输出 20160820/42a79759f284b767dfcb2a0197904287.jpg + $save_name = $info->getSaveName(); + + // 输出 42a79759f284b767dfcb2a0197904287.jpg + $new_filename = $info->getFilename(); + + $relative_url = $outer_req_url.'/'.str_replace('\\', '/', $save_name); + $result[] = [ + 'prefix'=>request()->domain().$outer_req_url,//访问前缀 + 'source_file_name'=>$source_file_name, + 'file_type'=>$file_type, + 'extension'=>$extension, + 'save_name'=>str_replace('\\', '/', $save_name), + 'new_filename'=>$new_filename, + 'real_path'=>$this->dealPath($root_path.'public'.$relative_url),//物理路径 + 'outer_req_url'=>request()->domain().$relative_url, + 'outer_req_relative_url'=>$relative_url, + 'size'=>$info->getSize(), + ]; + }else{ + //忽略错误 + // 上传失败获取错误信息 + $error[] = $file->getError(); + } + } + + $msg = '上传成功'; + $code = 0; + if(count($error) > 0){ + $msg = '上传失败;提示:'.implode(',', $error);//顺带返回错误信息 + $code = 1; + } + return Tools::set_res($code, $msg, $result); + } + + + /** + * desc:单文件 + * + * 【!!!必须确认php.ini中upload_max_filesize的配置足够大,否则上传失败但不会报错!!!】 + * + * author:wh + * @param string $file_upload_name 表单文件元素name值 + * @param int $size 大小,默认10M + * @param string $ext 允许的扩展 + * @return array + */ + function file($file_upload_name = 'file_upload', $size=10, $ext=''){ + if(!$this->uploadMaxFilesizeCheck($size)){ + return Tools::set_res(1, '上传文件受限'); + } + // 获取表单上传文件 例如上传了001.jpg + $file = request()->file($file_upload_name); + if(empty($file)){ + return Tools::set_res(1, '请上传文件'); + } + // 移动到框架应用根目录/uploads/ 目录下 + $outer_req_url = $this->file_dir.'/file'; + $physics_path = 'public'.$outer_req_url; + + $root_path = explode('vendor',__DIR__)[0]; + + $upload_dir = $root_path.$physics_path; + if(!is_dir($upload_dir)){ + mkdir($upload_dir, 0777, true); + } + if(empty($file)){ + return Tools::set_res(1, '上传文件不存在'); + } + + if(empty($ext)) $ext = $this->file_ext; + + $info = $file->validate(['size'=>$size * 1024 * 1024, 'ext'=>$ext])->move($upload_dir); + if($info){ + //原文件名 + $source_file_name = $info->getInfo('name'); + + //文件类型 + $file_type = $info->getInfo('type'); + + // 输出扩展名 jpg + $extension = $info->getExtension(); + + // 输出 20160820/42a79759f284b767dfcb2a0197904287.jpg + $save_name = $info->getSaveName(); + + // 输出 42a79759f284b767dfcb2a0197904287.jpg + $new_filename = $info->getFilename(); + + $relative_url = $outer_req_url.'/'.str_replace('\\', '/', $save_name); + $result = [ + 'prefix'=>request()->domain().$outer_req_url,//访问前缀 + 'source_file_name'=>$source_file_name, + 'file_type'=>$file_type, + 'extension'=>$extension, + 'save_name'=>str_replace('\\', '/', $save_name), + 'new_filename'=>$new_filename, + 'real_path'=>$this->dealPath($root_path.'public'.$relative_url),//物理路径 + 'outer_req_url'=>request()->domain().$relative_url, + 'outer_req_relative_url'=>$relative_url, + 'size'=>$info->getSize(), + ]; + return Tools::set_res(0, '上传成功', $result); + }else{ + // 上传失败获取错误信息 + return Tools::set_res(1, $file->getError()); + } + } + + /** + * desc:多文件 + * + * 【!!!必须确认php.ini中upload_max_filesize的配置足够大,否则上传失败但不会报错!!!】 + * + * author:wh + * @param string $file_upload_name 表单文件元素name值 + * @param int $size 单文件大小,默认10M + * @param string $ext 允许的扩展 + * @return array + */ + function files($file_upload_name = 'file_upload', $size=10, $ext=''){ + if(!$this->uploadMaxFilesizeCheck($size)){ + return Tools::set_res(1, '上传文件受限'); + } + + $root_path = explode('vendor',__DIR__)[0]; + // 获取表单上传文件 + $files = request()->file($file_upload_name); + if(empty($files)){ + return Tools::set_res(1, '请上传文件'); + } + $outer_req_url = $this->file_dir.'/file'; + $physics_path = 'public'.$outer_req_url; + $upload_dir = $root_path.$physics_path; + if(!is_dir($upload_dir)){ + mkdir($upload_dir, 0777, true); + } + + if(empty($ext)) $ext = $this->file_ext; + + $result = []; + $error = []; + foreach($files as $file){ + // 移动到框架应用根目录/uploads/ 目录下 + $info = $file->validate(['size'=>$size * 1024 * 1024, 'ext'=>$ext])->move($upload_dir); + if($info){ + //原文件名 + $source_file_name = $info->getInfo('name'); + + //文件类型 + $file_type = $info->getInfo('type'); + + // 输出扩展名 jpg + $extension = $info->getExtension(); + + // 输出 20160820/42a79759f284b767dfcb2a0197904287.jpg + $save_name = $info->getSaveName(); + + // 输出 42a79759f284b767dfcb2a0197904287.jpg + $new_filename = $info->getFilename(); + + $relative_url = $outer_req_url.'/'.str_replace('\\', '/', $save_name); + $result[] = [ + 'prefix'=>request()->domain().$outer_req_url,//访问前缀 + 'source_file_name'=>$source_file_name, + 'file_type'=>$file_type, + 'extension'=>$extension, + 'save_name'=>str_replace('\\', '/', $save_name), + 'new_filename'=>$new_filename, + 'real_path'=>$this->dealPath($root_path.'public'.$relative_url),//物理路径 + 'outer_req_url'=>request()->domain().$relative_url, + 'outer_req_relative_url'=>$relative_url, + 'size'=>$info->getSize(), + ]; + }else{ + //忽略错误 + // 上传失败获取错误信息 + $error[] = $file->getError(); + } + } + $msg = '上传成功'; + $code = 0; + if(count($error) > 0){ + $msg = '上传失败;提示:'.implode(',', $error);//顺带返回错误信息 + $code = 1; + } + return Tools::set_res($code, $msg, $result); + } + + + /** + * desc:单文件或多文件上传至阿里云OSS服务 + * + * author:wh + * @param array $config 配置 + * //阿里云oss配置 + 'aliyun_oss_config' => [ + //项目应用名称 + 'bucket'=>'wanlliuyinli-adm',//每创建一个bucket必须标明前缀,代表这个bucket属于哪个项目 + 'UserPrincipalName'=>'wanghua@1113242774600735.onaliyun.com', + 'Password'=>'0osE4cGo%tllnP1|uOQADPhM}Y?obR4U', + 'AccessKeyId'=>'LTAI5tPqn1n7jugviVoGqFfa', + 'AccessKeySecret'=>'BRoB5TdcUAFEuIR11BbN3R47Cm4Yep', + //https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints?spm=a2c4g.11186623.0.0.41ae2effA6oZar + 'region_id'=>'cn-chengdu',//这里配置不能包含oss-前缀,否则提示:Invalid signing region in Authorization header + //西南1(成都) + //oss-cn-chengdu + //oss-cn-chengdu.aliyuncs.com + //oss-cn-chengdu-internal.aliyuncs.com + 'endpoint'=>'oss-cn-chengdu.aliyuncs.com',//成都节点 + 'sts_endpoint'=>'sts.cn-chengdu.aliyuncs.com',//sts服务节点 + ] + * @param string $unique_id 唯一id,可以是用户id,可以是其它用于区分的标识,默认存放在(项目名称/控制器/方法/(唯一标识,可为空)/年月日/*.jpg) + * @param string $file_upload_name 文件上传控件的name值,必须带[],例如:file_upload[] + * @param string $oss_type oss类型,可选值:ali_cloud 阿里云,hua_wei 华为云(待扩展) + * @param int $size 单文件大小,默认10M + * @param string $ext 允许的文件扩展名 + * @return array + */ + function filesUploadToAliCloudOss($config,$unique_id='',$file_upload_name = 'file_upload',$size=10, $ext=''){ + return Mmodel::catch(function ()use ($config,$unique_id,$file_upload_name,$size,$ext){ + $ini_upload_max_filesize = str_replace('M','',ini_get('upload_max_filesize')); + if(($size > $ini_upload_max_filesize)){ + return Tools::set_res(1, '上传文件size受限,不能超过upload_max_filesize='.ini_get('upload_max_filesize').'M'); + } + // 获取表单上传文件 + $files = request()->file($file_upload_name); + if(empty($files)){ + return Tools::set_res(1, '请上传文件'); + } + //扩展 + if(empty($ext)) $ext = $this->file_ext; + //初始化阿里云OSS客户端 + $oss_obj= new Objects($config); + //默认存储地址 + $file_save_dir = $this->setFileSaveDir($unique_id); + $urls = []; + foreach($files as $file){ + if(!$file->isValid()){ + return Tools::set_res(1,'文件不合法'); + } + $check_size = $file->checkSize($size*1024*1024); + if(!$check_size){ + return Tools::set_res(1,'文件大小超限'); + } + $check_res = $file->checkExt($ext); + if(!$check_res){ + return Tools::set_res(1,'请上传合法文件'); + } + $extension = explode('.',$file->getInfo('name'))[1]; + $new_filename = 'file_'.Tools::rand_str(18).'.'.explode('.',$file->getInfo('name'))[1]; + //必须加后缀 + $filename = $file_save_dir.$new_filename; + + // 从文件流上传 + $res = $oss_obj->fileUpload($filename, $file->getInfo('tmp_name'));//上传之前的临时文件物理地址 + $urls[] = [ + 'prefix'=>explode('.com',$res['info']['url'])[0].'.com',//访问前缀 + 'source_file_name'=>$file->getInfo('name'),//源文件名 + 'size'=>$file->getInfo('size'),//大小(b) + 'extension'=>$extension,//扩展名 + 'file_type'=>$file->getInfo('type'),//文件后缀 + 'save_name'=>$filename,//保存文件名 + 'new_filename'=>$new_filename,//新文件名 + 'real_path'=>$res['info']['url'],//实际路径 + 'outer_req_url'=>$res['info']['url'],//外部访问地址 + 'outer_req_relative_url'=>$res['info']['url'],//外部访问相对地址 + ]; + } + return Tools::set_ok('ok', $urls); + }); + } + + /** + * desc:单文件上传至阿里云OSS + * + * author:wh + * @param array $config 配置 + * //阿里云oss配置 + 'aliyun_oss_config' => [ + //项目应用名称 + 'bucket'=>'wanlliuyinli-adm',//每创建一个bucket必须标明前缀,代表这个bucket属于哪个项目 + 'UserPrincipalName'=>'wanghua@1113242774600735.onaliyun.com', + 'Password'=>'0osE4cGo%tllnP1|uOQADPhM}Y?obR4U', + 'AccessKeyId'=>'LTAI5tPqn1n7jugviVoGqFfa', + 'AccessKeySecret'=>'BRoB5TdcUAFEuIR11BbN3R47Cm4Yep', + //https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints?spm=a2c4g.11186623.0.0.41ae2effA6oZar + 'region_id'=>'cn-chengdu',//这里配置不能包含oss-前缀,否则提示:Invalid signing region in Authorization header + //西南1(成都) + //oss-cn-chengdu + //oss-cn-chengdu.aliyuncs.com + //oss-cn-chengdu-internal.aliyuncs.com + 'endpoint'=>'oss-cn-chengdu.aliyuncs.com',//成都节点 + 'sts_endpoint'=>'sts.cn-chengdu.aliyuncs.com',//sts服务节点 + ] + * @param string $unique_id 唯一id,可以是用户id,可以是其它用于区分的标识,默认存放在(项目名称/控制器/方法/(唯一标识,可为空)/年月日/*.jpg) + * @param string $file_upload_name 文件上传控件的name值,例如:file_upload + * @param string $oss_type oss类型,可选值:ali_cloud 阿里云,hua_wei 华为云(待扩展) + * @param int $size 单文件大小,默认10M + * @param string $ext 允许的文件扩展名 + * @return array + */ + function fileUploadToAliCloudOss($config,$unique_id='',$file_upload_name = 'file_upload',$size=10, $ext=''){ + return Mmodel::catch(function ()use ($config,$unique_id,$file_upload_name,$size,$ext){ + $ini_upload_max_filesize = str_replace('M','',ini_get('upload_max_filesize')); + if(($size > $ini_upload_max_filesize)){ + return Tools::set_res(1, '上传文件size受限,不能超过upload_max_filesize='.ini_get('upload_max_filesize').'M'); + } + // 获取表单上传文件 + $file = request()->file($file_upload_name); + if(empty($file)){ + return Tools::set_res(1, '请上传文件'); + } + //扩展 + if(empty($ext)) $ext = $this->file_ext; + //初始化阿里云OSS客户端 + $oss_obj= new Objects($config); + //默认存储地址 + $file_save_dir = $this->setFileSaveDir($unique_id); + $urls = []; + if(!$file->isValid()){ + return Tools::set_res(1,'文件不合法'); + } + $check_size = $file->checkSize($size*1024*1024); + if(!$check_size){ + return Tools::set_res(1,'文件大小超限'); + } + $check_res = $file->checkExt($ext); + if(!$check_res){ + return Tools::set_res(1,'请上传合法文件'); + } + $extension = explode('.',$file->getInfo('name'))[1]; + $new_filename = 'file_'.Tools::rand_str(18).'.'.explode('.',$file->getInfo('name'))[1]; + //必须加后缀 + $filename = $file_save_dir.$new_filename; + + // 从文件流上传 + $res = $oss_obj->fileUpload($filename, $file->getInfo('tmp_name'));//上传之前的临时文件物理地址 + $urls[] = [ + 'prefix'=>explode('.com',$res['info']['url'])[0].'.com',//访问前缀 + 'source_file_name'=>$file->getInfo('name'),//源文件名 + 'size'=>$file->getInfo('size'),//大小(b) + 'extension'=>$extension,//扩展名 + 'file_type'=>$file->getInfo('type'),//文件后缀 + 'save_name'=>$filename,//保存文件名 + 'new_filename'=>$new_filename,//新文件名 + 'real_path'=>$res['info']['url'],//实际路径 + 'outer_req_url'=>$res['info']['url'],//外部访问地址 + 'outer_req_relative_url'=>$res['info']['url'],//外部访问相对地址 + ]; + return Tools::set_ok('ok', $urls); + }); + } + + /** + * desc:设置文件保存目录 + * author:wh + * @param string $unique_id 唯一id,可以是用户id,可以是其它用于区分的标识 + * @return string + */ + function setFileSaveDir($unique_id=''){ + if($unique_id){ + $unique_id = $unique_id.'/'; + } + //项目名称 + $project_name = Tools::get_project_name(); + //控制器方法路径 + $ctl = strtolower(request()->controller().'/'.request()->action()); + return $project_name.'/'.$ctl.'/'.$unique_id.date('Ymd').'/';//日期 + } + /** + * desc:检测size是否在ini配置范围 + * + * author:wh + * @param $size + * @return bool + */ + private function uploadMaxFilesizeCheck($size){ + $ini_upload_max_filesize = str_replace('M','',ini_get('upload_max_filesize')); + return $size<=$ini_upload_max_filesize; + } + + + /** + * desc:上传base64格式图片【文件不用base64传输】 + * author:wh + * @param string $file_upload_name + * @param int $size + * @param array $ext + * @return array + * @throws \Exception + */ + function uploadBase64Img($file_upload_name = 'file_upload', $size=10, $ext=''){ + $source = input($file_upload_name, 'file_upload'); + $file_size = strlen($source); + if($file_size > $size * 1024 * 1024){ + return Tools::set_res(1, '上传失败,大小限制'); + } + if(preg_match('/^(data:\s*image\/(\w+);base64,)/', $source, $result) < 1){ + return Tools::set_res(1, '上传文件不合法'); + } + $type = $result[2]; + + if(empty($ext)) $ext = $this->file_ext; + + if(!in_array($type, explode($ext))){ + return Tools::set_res(1, '上传失败,类型限制'); + } + $result = base64_image_content($source); + if(false === $result){ + return Tools::set_res(1, '上传失败'); + } + return Tools::set_res(0, '上传成功', $result); + } + + /** + * desc:批量上传base64格式图片【文件不用base64传输】 + * author:wh + * @param string $file_upload_name + * @param int $size + * @param array $ext + * @return array + * @throws \Exception + */ + function uploadBase64Imgs($file_upload_name = 'file_upload', $size=10, $ext=''){ + $source_list = input($file_upload_name, 'file_upload'); + if(!is_array($source_list)){ + return Tools::set_res(1, '参数错误'); + } + + if(empty($ext)) $ext = $this->file_ext; + + $result = []; + $error = []; + foreach ($source_list as $source){ + $file_size = strlen($source); + if($file_size > $size * 1024 * 1024){ + $error[] = '上传失败,大小限制'; + continue; + } + if(preg_match('/^(data:\s*image\/(\w+);base64,)/', $source, $temp) < 1){ + $error[] = '上传文件不合法'; + continue; + } + $type = $temp[2]; + if(!in_array($type, explode(',', $ext))){ + $error[] = '类型限制'; + continue; + } + $up_result = base64_image_content($source); + if(false === $up_result){ + $error[] = '上传失败'; + continue; + } + $result[] = $up_result; + } + + $msg = '上传成功'; + $code = 0; + if(count($error) > 0){ + $msg = '上传失败;提示:'.implode(',', $error);//顺带返回错误信息 + $code = 1; + } + return Tools::set_res($code, $msg, $result); + } + + /** + * desc:优化路径格式 + * author:wh + */ + private function dealPath(string $path){ + $str = str_replace('\\','/',$path); + + return str_replace('//','/',$str); + } + + + /** + * desc:curl上传文件,php上传文件到远程服务器地址 + * + * [php curl模拟文件上传] + * + * 依赖库:"guzzlehttp/guzzle": "^7.8", + * + * author:wh + * @param $url + * @param $params + * @param false[] $config + */ + function uploadCurlFiles($url,$params, $config=['verify' => false]){ + if(empty($this->unique_key)){ + throw new \Exception('UNIQUE_KEY 不能为空'); + } + if(empty($this->file_lists_urls)){ + throw new \Exception('文件地址列表不能为空'); + } + // 创建 Guzzle HTTP 客户端实例 + $client = new Client($config); + + // 定义要上传的文件列表 + $files = [ + //[ + // 'name' => 'file1', // 服务器接收的文件字段名 + // 'contents' => Utils::tryFopen('D:\wanghua\projects\big_world_projects\universal_gravitation\public\uploads\20240506\dc1c441d81d48565ac6817b89d0f8bef.mp4', 'r'), // 文件路径 + // 'filename' => 'dc1c441d81d48565ac6817b89d0f8bef.mp4', // 文件名,可自定义 + //], + //[ + // 'name' => 'files', + // 'contents' => Utils::tryFopen('D:\wanghua\projects\big_world_projects\universal_gravitation\public\uploads\20240506\f80179b23b60619fba8033bfd64f8817.mp4', 'r'), + // 'filename' => 'f80179b23b60619fba8033bfd64f8817.mp4', + //], + //['name'=>'prompt','contents'=>$prompt], + //['name'=>'tts_url','contents'=>$tts_url] + // 可以继续添加更多文件... + ]; + //print_r($params); + foreach ($params as $key=>$val){ + $files[] = ['name'=>$key,'contents'=>$val]; + } + $controller = strtolower(request()->controller()); + $action = strtolower(request()->action()); + $save_path = Tools::get_root_path() . "public/uploads/{$controller}/{$action}/".$this->unique_key; + if(!file_exists($save_path)){ + mkdir($save_path,0777,true); + } + + //文件远程可访问地址列表 + foreach ($this->file_lists_urls as $name=>$video_url) { + //处理可用地址(可能存储在本地或oss) + $video_url = $this->get_usable_url($video_url); + $files[] = [ + 'name'=>"files",//服务器接收的文件字段名 + //读取地址文件流(本地地址或远程地址均可,本地真实物理地址也行) + 'contents'=>Utils::tryFopen($video_url, 'r'), + 'filename'=>$name,//可自定义 文件名 + ]; + } + + // 构建多部分表单数据 + $multipartStream = new MultipartStream($files); + $boundary = $multipartStream->getBoundary(); + $headers = [ + 'Content-Type' => "multipart/form-data; boundary={$boundary}", + ]; + + // 准备请求体 + $body = $multipartStream->getContents(); + + // 发起 POST 请求 + try { + $postinfo = [ + 'headers' => $headers, + 'body' => $body, + ]; + //上传 + $response = $client->request('POST', $url, $postinfo); + // 处理响应 + //$responseBody = (string) $response->getBody(); + // 检查状态码 + //if ($response->getStatusCode() !== 200) { + // return false; + //} + return $response;//返回上传结果 + } catch (\GuzzleHttp\Exception\RequestException $e) { + // 错误处理 + //echo "Error: " . $e->getMessage(); + Tools::error_txt_log($e); + return false; + } + } + + /** + * desc:获取视频可用地址(兼容远程地址或本地地址) + * author:wh + * @param $video_url + * @return string|string[] + */ + private function get_usable_url($video_url){ + if(strpos($video_url,'http')!==false){ + return $video_url;//远程地址 + } + return Tools::get_root_path().$video_url;//本地地址 + } + + + +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/file/upload/README.MD b/admin/vendor/wanghua/general-utility-tools-php/src/file/upload/README.MD new file mode 100644 index 0000000..df6ecdf --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/file/upload/README.MD @@ -0,0 +1 @@ +## 文件上传类库 \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/framework/BaseController.php b/admin/vendor/wanghua/general-utility-tools-php/src/framework/BaseController.php new file mode 100644 index 0000000..1ae2a73 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/framework/BaseController.php @@ -0,0 +1,219 @@ +checkMaintain(); + if ($chm['is_maintain']) { + if ($chm['users_ids']) { + //解析openid + if (!in_array(api_user_info('id'), explode(',', $chm['users_ids']))) { + //白名单之外维护中 + //Tools::log_to_write_txt([ + // '维护测试'=>$chm['users_ids'], + // 'my'=>index_user_openid() + //]); + return $this->error($chm['msg']); + } + } else { + //不存在,直接维护中 + return $this->error($chm['msg'] . '!'); + } + } + //校验系统维护状态 end + } + + /** + * desc:空控操作 + * author:wh + */ + function _empty($info='') + { + + } + /** + * desc:检查路径维护状态 + * + * “===”完全匹配,不能使用like + * + * author:wh + */ + protected function checkMaintain(){ + $configs = Db::table(TabConf::$fa_sys_maintain_config) + ->where('status','1') + ->cache() + ->select(); + + //模块 + $strmodule = request()->module(); + foreach ($configs as $config){ + if($strmodule == $config['url']){ + //模块维护中 + return ['is_maintain'=>true,'msg'=>$config['msg'],'users_ids'=>$config['users_ids']]; + } + } + + //模块/控制器 + $strcontroller = strtolower(request()->module().'/'.request()->controller()); + foreach ($configs as $config){ + if($strcontroller == $config['url']){ + //模块维护中 + return ['is_maintain'=>true,'msg'=>$config['msg'],'users_ids'=>$config['users_ids']]; + } + } + + //模块/控制器/方法 + $straction = strtolower(request()->module().'/'.request()->controller().'/'.request()->action()); + foreach ($configs as $config){ + if($straction == $config['url']){ + //模块维护中 + return ['is_maintain'=>true,'msg'=>$config['msg'],'users_ids'=>$config['users_ids']]; + } + } + + //未维护 + return ['is_maintain'=>false,'msg'=>'服务运行中']; + } + /** + * desc:清空系统缓存 + * + /index/Test/clearCache + * + * author:wh + */ + function clearCache(){ + + Tools::clear_cache(); + } + /** + * eg:/index/test/buildTablesConf + * + * desc:构建统一的表名配置 + * + * author:wh + */ + function buildTablesConf(){ + + (new MySqlTools())->buildTablesConf(); + + } + + /** + * desc:创建&更新接口文档 + * + * 默认存放在/public/api_docs/api_list.md + * + * author:wh + */ + function buildApiDoc() + { + $obj = new ApiDocument(); + //根据自己的实际情况设置直接继承类(仅供参考) + $obj->extends_base_class = 'app\\api\\controller\\BaseHttpApi,wanghua\\general_utility_tools_php\\framework\\base\\BaseWechatAuthController'; + //设置过滤的类(仅供参考) + $obj->setFilterClass([ + 'BaseCommonController', + 'BaseHttpApi', + 'BaseWssApi', + 'Wsspush', + 'BaseController', + 'BaseAuthController', + //'BaseWechatAuthController', + 'BasePublicController' + ]); + $obj->setFilterFunction([ + 'buildApiDoc', + 'buildTablesConf', + 'checkfailed', + 'clearCache', + 'defaultAuth', + 'operateLog', + 'checkMaintain', + '_empty' + ]); + //构建接口文档 + $obj->buildDoc(); + //生成html + $html = $obj->buildApiDocHtml(); + $path = Tools::get_root_path().'/public/api_docs/'; + if(!file_exists($path)){ + mkdir($path,0777,true); + } + file_put_contents($path.'api_list.html',$html); + echo "api_list.html"; + + } + + /** + * desc:用户操作日志 + * author:wh + */ + protected function operateLog($msg,$users_id=0,$username='',$name=''){ + $tbname = 'fa_users_operate_log'; + $sql = " + CREATE TABLE IF NOT EXISTS `{$tbname}` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `users_id` int(10) unsigned DEFAULT '0' COMMENT '用户ID', + `username` varchar(15) DEFAULT '' COMMENT '用户名', + `name` varchar(20) DEFAULT '' COMMENT '名称', + `url` varchar(60) DEFAULT '' COMMENT 'URL', + `msg` varchar(90) DEFAULT '' COMMENT '做了什么', + `ip` varchar(30) DEFAULT '' COMMENT '登录ip', + `input` text COMMENT '提交参数', + `create_time` timestamp NULL DEFAULT NULL COMMENT '登陆时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=54 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户操作日志';"; + + if(cache('cache_db_tables_now_project')){ + $table_arr = cache('cache_db_tables_now_project'); + }else{ + $table_arr = Tools::get_tables(); + } + //表是否存在 + if(!in_array($tbname,$table_arr)){ + cache('cache_db_tables_now_project',$table_arr); + Db::execute($sql); + } + + $data = [ + 'users_id'=>$users_id, + 'username'=>$username, + 'name'=>$name, + 'url'=>request()->url(), + 'msg'=>$msg, + 'ip'=>request()->ip(), + 'input'=>json_encode(input(),JSON_UNESCAPED_UNICODE), + ]; + Db::table($tbname)->data($data)->insert(); + + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/framework/README.MD b/admin/vendor/wanghua/general-utility-tools-php/src/framework/README.MD new file mode 100644 index 0000000..789b3a0 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/framework/README.MD @@ -0,0 +1 @@ +### this classes only to thinkphp5+ \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/framework/base/BaseAuthController.php b/admin/vendor/wanghua/general-utility-tools-php/src/framework/base/BaseAuthController.php new file mode 100644 index 0000000..a73e32d --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/framework/base/BaseAuthController.php @@ -0,0 +1,80 @@ +auth_err_redirect_url = $err_redirect_url; + } + + } + + /** + * desc:默认鉴权 + * author:wh + * @return bool + */ + function defaultAuth(){ + $params = input(); + if(empty($params['nonce'])){ + Tools::log_to_write_txt(['服务被拒绝,鉴权参数缺失:nonce。params'=>input()]); + //跳转至错误中转控制器 + return $this->response($this->auth_err_redirect_url,['title'=>'服务被拒绝. permission denied']); + } + if(empty($params['timestamp'])){ + Tools::log_to_write_txt(['服务被拒绝,鉴权参数缺失:timestamp。params'=>input()]); + //跳转至错误中转控制器 + return $this->response($this->auth_err_redirect_url,['title'=>'服务被拒绝. permission denied.']); + } + if(empty($params['sign'])){ + Tools::log_to_write_txt(['服务被拒绝,鉴权参数缺失:sign。params'=>input()]); + //跳转至错误中转控制器 + return $this->response($this->auth_err_redirect_url,['title'=>'服务被拒绝. permission denied。']); + } + $sign = $params['sign']; + unset($params['sign']); + if(Tools::signature($params) != $sign){ + + Tools::log_to_write_txt(['签名失败,服务被拒绝.'=>input()]); + + //跳转至错误中转控制器 + return $this->response($this->auth_err_redirect_url,['title'=>'服务被拒绝. permission denied!']); + } + return true; + } + protected function response($url,$params){ + + if(Request::instance()->isAjax() || Request::instance()->isPost()){ + return json(Tools::set_res(500,$params['title'],['url'=>$url])); + } + //跳转至错误中转控制器 + return $this->redirect(url($url,$params)); + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/framework/base/BasePublicController.php b/admin/vendor/wanghua/general-utility-tools-php/src/framework/base/BasePublicController.php new file mode 100644 index 0000000..e2f9fa6 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/framework/base/BasePublicController.php @@ -0,0 +1,29 @@ +assign('index_msg', cache('index_msg_alert_cache_time')); + + //线上环境加载微信授权 + if (config('sys_env') == 'PROD') { + $wx_user_info = session('wx_user_info'); + if (empty($wx_user_info['openid'])) { + //重定向之前,保存当前url, 在获取授权信息之后,回跳到授权之前的网页地址 + session('redirect_before_url_session', request()->url(true)); + $ver = Tools::get_thinkphp_version(); + if($ver == '5.0'){ + $url = url('index/Wexinauth/usrAuth','',false,true); + }else{ + $url = request()->domain().url('index/Wexinauth/usrAuth'); + } + //没有则重定向去授权 + return $this->redirect($url); + } + + $this->saveWechatUser($wx_user_info); + } + + + + + } + + + /** + * desc:周期更新当前用户信息 + * author:wh + * @param $wx_user_info + */ + private function saveWechatUser($wx_user_info) + { + try { + $wechat_user = $this->getWxUserByOpenid($wx_user_info['openid']); + if (empty($wechat_user)) { + return $this->insertInfo($wx_user_info); + } + //扩展,按周期更新,而不是不更新 + if (empty($wechat_user['update_time']) || time() - strtotime($wechat_user['update_time']) > 5 * 3600) { + + return $this->updateUser($wx_user_info); + } + + + } catch (\Exception $e) { + Tools::log_to_write_txt([ + 'error' => '存储异常.' . $e->getMessage(), + 'wx_user_info' => $wx_user_info, + 'error_info' => $e->getTraceAsString() + ]); + } + } + + /** + * desc:新增微信用户信息 + * author:wh + * @param $wx_user_info \app\index\model\微信用户 + * + * { + * "openid":"or9D2vs863Ky5Py2ovkAiu9XFLO4", + * "nickname":"起源果蔬副食大华", + * "sex":0, + * "language":"", + * "city":"", + * "province":"", + * "country":"", + * "headimgurl":"https://thirdwx.qlogo.cn/mmopen/vi_32/joiaA475nx3fJiaqx0ibdnWo4A7Q3uCgu2hsribI0ATLItORjuUgCSP8mCaBkqL61ibGojib4pQYX1djUhZpF5zoqpSg/132", + * "privilege":[] + * } + */ + private function insertInfo(array $wx_user_info) + { + + if (isset($wx_user_info['privilege'])) { + $wx_user_info['privilege'] = json_encode($wx_user_info['privilege']); + } + $data = [ + 'nickname' => $wx_user_info['nickname'], + 'country' => $wx_user_info['country'], + 'province' => $wx_user_info['province'], + 'city' => $wx_user_info['city'], + 'headimage' => $wx_user_info['headimgurl'], + 'language' => $wx_user_info['language'], + 'openid' => $wx_user_info['openid'], + 'unionid' => isset($wx_user_info['unionid']) ? $wx_user_info['unionid'] : '', + 'privilege' => $wx_user_info['privilege'], + 'sex' => $wx_user_info['sex'], + //'arm_group' => '', + //'score' => 0,//积分 + //'group_buy_earnings' => 0,//拼团收益 + //'water_drop_balance' => 0,//水滴 + ]; + Db::table($this->wechat_user_table_name) + ->data($data) + ->insert(); + } + + /** + * desc:更新当前用户信息 + * + * author:wh + */ + private function updateUser($wx_user_info) + { + $data = [ + 'nickname' => $wx_user_info['nickname'], + 'headimage' => $wx_user_info['headimgurl'], + 'unionid' => isset($wx_user_info['unionid']) ? $wx_user_info['unionid'] : '', + ]; + Db::table($this->wechat_user_table_name) + ->data($data) + ->where('openid', index_user_openid()) + ->update(); + } + + /** + * desc:根据unionid获取用户信息 + * author:wh + * @param string $unionid + */ + private function getWxUserByUnionid(string $unionid) + { + return Db::table($this->wechat_user_table_name) + ->field('id') + ->where('unionid', $unionid) + ->find(); + } + + /** + * desc:获取微信用户信息 + * author:wh + * @param string $openid + */ + private function getWxUserByOpenid(string $openid) + { + return Db::table($this->wechat_user_table_name) + //->field('id') + ->where('openid', $openid) + ->find(); + } + + /** + * desc:根据openid获取昵称 + * author:wh + * @param string $openid + */ + private function getNicknameByOpenid(string $openid) + { + return Db::table($this->wechat_user_table_name) + ->where('openid', $openid) + ->value('nickname'); + } + + + + +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/ftp/Ftp.php b/admin/vendor/wanghua/general-utility-tools-php/src/ftp/Ftp.php new file mode 100644 index 0000000..e8c8806 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/ftp/Ftp.php @@ -0,0 +1,213 @@ +conn_id = ftp_connect($host, $port); + if(!$this->conn_id) throw new \Exception("FTP服务器连接失败"); + $login_res = ftp_login($this->conn_id, $user, $password); + if(!$login_res) throw new \Exception("FTP服务器登陆失败"); + ftp_pasv($this->conn_id, false); // true打开被动模拟,默认关闭,如果总是失败,可试试true + } + + /** + * desc:打开被动模拟,登录后使用 + * author:wh + */ + function set_ftp_pasv(){ + ftp_pasv($this->conn_id, true); // true打开被动模拟 + } + + /** + * 上传文件 + * param $path 本地文件路径[包含文件名] + * param $newPath 目标目录[包含文件名][默认存放在ftp用户被授权的根路径] + * param bool $type 若目标目录不存在则新建 + */ + function up_file($path, $newPath, $type = true) + { + if ($type) $this->dir_mkdirs($newPath); + $this->off = ftp_put($this->conn_id, $newPath, $path, FTP_BINARY); + if (!$this->off) + return ['code'=>500, 'msg'=>'文件上传失败,请检查权限及路径是否正确!']; + return ['code'=>200, 'msg'=>'文件移动成功']; + } + + /** + * 移动文件 + * 特别注意: + * $path 如果是当前目录,则必须以 “/”开始 + * 正确:/test2.zip + * 错误:test2.zip + * param $path 原路径(含文件名) + * param $newPath 新路径(含文件名) + * param bool $type 若目标目录不存在则新建 + */ + function move_file($path, $newPath, $ftpbasedir, $type = true) + { + if ($type) $this->ftp_mksubdirs($ftpbasedir,$newPath); + $this->off = ftp_rename($this->conn_id, $path, $newPath); + if (!$this->off) + return ['code'=>500, 'msg'=>'文件移动失败,请检查权限及原路径是否正确!']; + return ['code'=>200, 'msg'=>'文件移动成功']; + } + + /** + * 文件改名 + * param $path 原路径 + * param $newPath 新路径 + */ + function rename_file($path, $newPath) + { + $this->off = ftp_rename($this->conn_id, $path, $newPath); + if (!$this->off) + return ['code'=>500, 'msg'=>'文件改名失败,请检查权限及原路径是否正确!']; + return ['code'=>200, 'msg'=>'文件改名成功']; + } + + /** + * 复制文件 + * 说明:由于FTP无复制命令,本方法变通操作为:下载后再上传到新的路径 + * param $path 原路径 + * param $newPath 新路径 + * param bool $type 若目标目录不存在则新建 + */ + function copy_file($path, $newPath, $type = true) + { + $downPath = "c:/tmp.dat"; + $this->off = ftp_get($this->conn_id, $downPath, $path, FTP_BINARY);// 下载 + if (!$this->off) + return ['code'=>500, 'msg'=>'文件复制失败,请检查权限及原路径是否正确!']; + + $this->up_file($downPath, $newPath, $type); + return ['code'=>200, 'msg'=>'文件复制成功']; + } + + /** + * 删除文件 + * param $path 路径 + */ + function del_file($path) + { + $this->off = ftp_delete($this->conn_id, $path); + if (!$this->off) + return ['code'=>500, 'msg'=>'文件删除失败,请检查权限及路径是否正确!']; + + return ['code'=>200, 'msg'=>'文件删除成功']; + } + + /** + * desc:删除给定目录 + * 注: + * 删除多个目录需重复调用 + * author:wh + * param $dir + * return bool + */ + function del_dir($dir){ + + + $children = ftp_nlist ($this->conn_id, $dir); + if(!$children) { + ftp_rmdir($this->conn_id, $dir); + + return true; + } + + throw new \Exception('请确保目录中无内容'); + + } + + + /** + * desc:生成目录 + * author:wh + * param $ftpcon + * param $ftpbasedir + * param $ftpath + */ + function ftp_mksubdirs($ftpbasedir,$ftpath){ + ftp_chdir($this->conn_id, $ftpbasedir); // /var/www/uploads + $parts = explode('/',$ftpath); // 2013/06/11/username + + if(strpos($parts[count($parts)-1], '.')){ + unset($parts[count($parts)-1]); + } + + foreach($parts as $part){ + if(!ftp_chdir($this->conn_id, $part)){ + ftp_mkdir($this->conn_id, $part); + ftp_chdir($this->conn_id, $part); + //ftp_chmod($this->conn_id, 0777, $part); + } + } + } + + /** + * desc:使用ftp连接方式下载文件 + * author:wh + * param string $local_file 文件本地的路径(如果文件已经存在,则会被覆盖)。 + * param string $remote_file 文件的远程路径。[默认查找ftp用户被授权的根路径] + * return array + */ + function download(string $local_file, string $remote_file){ + // 进行ftp下载[默认保存在本地根目录] + if (!ftp_get($this->conn_id, $local_file, $remote_file, FTP_BINARY)) { + return ['code'=>500, 'msg'=>'ftp download fail']; + } + return ['code'=>200, 'msg'=>'ftp download success']; + } + + /** + * desc:判断ftp文件大小, 如果>-1, 说明文件存在;否则不存在. + * author:wh + * param $remote_file + * return int + */ + function ftp_size($remote_file){ + return ftp_size($this->conn_id, $remote_file); + } + + /** + * desc:返回当前目录 + * author:wh + * return false|string + */ + function ftp_pwd(){ + return ftp_pwd($this->conn_id); + } + /** + * 关闭FTP连接 + */ + function close() + { + ftp_close($this->conn_id); + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/ftp/README.md b/admin/vendor/wanghua/general-utility-tools-php/src/ftp/README.md new file mode 100644 index 0000000..29b9a72 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/ftp/README.md @@ -0,0 +1,21 @@ +# PHP ftp类库 +#### 实现用ftp方式上传、复制、移动、重命名、删除、下载文件 + +##### 案例代码: +``` + $ftp = new Ftp();//初始化 + //$ftp->up_file('D:\whua\test.zip','test.zip'); // 上传文件 + //$ftp->rename_file('test.zip','test-rename.zip'); // 移动文件 + //$ftp->move_file('/test2.zip','/testdir2/test2.zip', '/www/wwwroot/testftp/'); // 移动文件2 + //$ftp->copy_file('test.zip','test-copy.zip'); // 复制文件 + //$ftp->del_file('/www/aaa.txt'); // 删除具体文件 + $ftp->del_dir('/www'); // 删除目录(错误示范),必须精确到具体文件夹删除 + $ftp->del_dir('/ftp/test/copy-test/a-dir'); // 删除目录(正确示范),必须精确到具体文件夹删除,批量删除请多次调用 + + //下载文件 + //$ftp->download('D:\whua\projects\zc_game_admin3.0\data\local.zip', 'test.zip'); + //$ftp->close(); // 关闭FTP连接 + + //dump($ftp->ftp_pwd());die; + //dump($ftp->ftp_size('test2.zip'));die; +``` \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/AliCloudChatGPT.php b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/AliCloudChatGPT.php new file mode 100644 index 0000000..8846433 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/AliCloudChatGPT.php @@ -0,0 +1,188 @@ +messages[] = $item; + } + } + } + /** + * desc:定制个性(多模态) + * author:wh + * @param array $customize + */ + function setCustomizeMulti($customize = []) + { + if ($customize) { + foreach ($customize as $item) { + $this->messages[] = $item; + } + } + } + + function setBefore($describe = []) + { + if ($describe) { + foreach ($describe as $item) { + $this->messages[] = $item; + } + } + } + + function setAfter($describe = []) + { + if ($describe) { + foreach ($describe as $item) { + $this->messages[] = $item; + } + } + } + + private function check() + { + if (empty($this->url) || empty($this->apiKey) || empty($this->model)) { + throw new Exception('PARAMS ERROR'); + } + } + + function chat($question = '', $config = [], &$answer_json_arr) + { + $answer = ''; + $this->curlPostChat($question, $config, function ($ch, $data) use ($question, &$answer, &$answer_json_arr) { + $answer_json_arr[] = $data; + $answer .= $data; + echo $data; + ob_flush(); + flush(); + return strlen($data); + }); + return $answer; + } + + function returnAnswer($question = '', $config = [], &$answer_json_arr) + { + $answer = ''; + $this->curlPostChat($question, $config, function ($ch, $data) use ($question, &$answer, &$answer_json_arr) { + $answer_json_arr[] = $data; + $answer .= $data; + return strlen($data); + }); + return $answer; + } + + private function curlPostChat($question = '', $config = [], $callback) + { + $url = $this->url; + $apiKey = $this->apiKey; + $model = $this->model; + $headers = ["Authorization: Bearer $apiKey", 'Accept: application/json', 'Content-Type: application/json',]; + $post_msg_body = ["model" => $model, 'stream' => true, 'chatId'=>$this->chatId]; + if ($config) { + foreach ($config as $key => $val) { + $post_msg_body[$key] = $val; + } + } + if ($question) { + $this->messages[] = ["role" => "user", "content" => $question]; + } + $post_msg_body['messages'] = $this->messages; + $postData = json_encode($post_msg_body); + $this->post_msg_body = $post_msg_body; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); + curl_setopt($ch, CURLOPT_WRITEFUNCTION, $callback); + curl_exec($ch); + } + + + + /** + * desc:多模态场景chat(如多图多轮对话) + * author:wh + * @param $images + * @param $text + * @return mixed + * @throws \Exception + */ + function generateMultimodalContent($images, $text) + { + $url = $this->url; + $apiKey = $this->apiKey; + $model = $this->model; + $messages = [ + [ + 'role' => 'user', + 'content' => array_map(function ($image) { + return ['image' => $image]; + }, $images), + ], + ]; + + if (!empty($text)) { + $messages[0]['content'][] = ['text' => $text]; + } + + $data = [ + 'model' => $model, + 'input' => [ + 'messages' => $messages, + ], + ]; + + $options = [ + 'http' => [ + 'header' => "Authorization: Bearer $apiKey\r\n" . + "Content-Type: application/json\r\n", + 'method' => 'POST', + 'content' => json_encode($data), + ], + ]; + + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + + if ($result === FALSE) { + throw new \Exception('Failed to fetch data from API'); + } + + return json_decode($result, true); + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/BaseChat.php b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/BaseChat.php new file mode 100644 index 0000000..9992d9c --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/BaseChat.php @@ -0,0 +1,42 @@ +chatId = Tools::getMillisecond(); + } + + /** + * desc:将字符串里含有json_encode后的一维数组提取出来 + * @param $str + * @return string + */ + function dealstr($str){ + $rep_str = str_replace("\n",'',$str); + $rep_str = str_replace("\r",'',$rep_str); + $rep_str = str_replace(" ",'',$rep_str); + + $start_sit = strpos($rep_str,"[{"); + $end_sit = strpos($rep_str,"}]"); + + $last_json_str = substr($rep_str,$start_sit,$end_sit-$start_sit); + + $last_json_str.="}]"; + + return $last_json_str; + } + +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/C62239E6C0F995AE1FDC00D18B7905EAC.php b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/C62239E6C0F995AE1FDC00D18B7905EAC.php new file mode 100644 index 0000000..038292c --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/C62239E6C0F995AE1FDC00D18B7905EAC.php @@ -0,0 +1 @@ +messages[] = $item; } } } function sB3250714FE96BE2F5E2D7798123A1B94($describe = []) { if ($describe) { foreach ($describe as $item) { $this->messages[] = $item; } } } function s326723D163240094B8C736D3A1A5CAD3($describe = []) { if ($describe) { foreach ($describe as $item) { $this->messages[] = $item; } } } private function c2111E682402A3955B616FA5795DEDE4F() { if (empty($this->url) || empty($this->apiKey) || empty($this->model)) { throw new Exception('PARAMS ERROR'); } } function cE0323A9039ADD2978BF5B49550572C7C($question = '', $config = [], &$answer_json_arr) { $answer = ''; $this->c8ECA4C82960B4611B076D51EF8ADF979($question, $config, function ($ch, $data) use ($question, &$answer, &$answer_json_arr) { $answer_json_arr[] = $data; $answer .= $data; echo $data; ob_flush(); flush(); return strlen($data); }); return $answer; } function crtque($question = '', $config = [], &$answer_json_arr) { $answer = ''; $this->c8ECA4C82960B4611B076D51EF8ADF979($question, $config, function ($ch, $data) use ($question, &$answer, &$answer_json_arr) { $answer_json_arr[] = $data; $answer .= $data; return strlen($data); }); return $answer; } private function c8ECA4C82960B4611B076D51EF8ADF979($question = '', $config = [], $callback) { $url = $this->url; $apiKey = $this->apiKey; $model = $this->model; $headers = ["Authorization: Bearer $apiKey", 'Accept: application/json', 'Content-Type: application/json',]; $post_msg_body = ["model" => $model, 'stream' => true,]; if ($config) { foreach ($config as $key => $val) { $post_msg_body[$key] = $val; } } if ($question) { $this->messages[] = ["role" => "user", "content" => $question]; } $post_msg_body['messages'] = $this->messages; $postData = json_encode($post_msg_body); $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); curl_setopt($ch, CURLOPT_WRITEFUNCTION, $callback); curl_exec($ch); } function getchatgptresponse($question = '', $config = []) { $url = $this->url; $apiKey = $this->apiKey; $model = $this->model; $headers = ["Authorization: Bearer $apiKey", "Content-Type: application/json"]; $post_msg_body = ["model" => $model, 'stream' => false]; if ($config) { foreach ($config as $key => $val) { $post_msg_body[$key] = $val; } } if ($question) { $this->messages[] = ["role" => "user", "content" => $question]; } $post_msg_body['messages'] = $this->messages; $post_msg_body = json_encode($post_msg_body); $ch = curl_init($url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_msg_body); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); if (curl_error($ch)) { return ['code' => curl_errno($ch), 'msg' => curl_error($ch)]; } else { curl_close($ch); return ['code' => 200, 'msg' => 'cURL ok', 'data' => $response]; } } private function pCD736318A127E12426EED8700FF2BF28($data) { if (@json_decode($data)->choices[0]->message->content) { return json_decode($data)->choices[0]->message->content; } $data = str_replace('data: {', '{', $data); $data = rtrim($data, "\n\n"); if (strpos($data, 'data: [DONE]') !== false) { return 'data: [DONE]'; } else { if (false !== strpos($data, "\n\n")) { $exp_arr = explode("\n\n", $data); $str = ''; try { for ($i = 0; $i < count($exp_arr) - 1; $i++) { $jsondecode_arr = json_decode($exp_arr[$i], true); $str .= $jsondecode_arr['choices'][0]['delta']['content']; } return $str; } catch (\Exception $e) { return $str; } } $data = @json_decode($data, true); if (!is_array($data)) { return ''; } if ($data['choices'][0]['finish_reason'] == 'stop') { return 'data: [DONE]'; } elseif ($data['choices'][0]['finish_reason'] == 'length') { return 'data: [CONTINUE]'; } return $data['choices'][0]['delta']['content'] ?? ''; } } } \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/ChatGPT.php b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/ChatGPT.php new file mode 100644 index 0000000..3d4ea44 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/ChatGPT.php @@ -0,0 +1,179 @@ +messages[] = $item; + } + } + } + + function setBefore($describe = []) + { + if ($describe) { + foreach ($describe as $item) { + $this->messages[] = $item; + } + } + } + + function setAfter($describe = []) + { + if ($describe) { + foreach ($describe as $item) { + $this->messages[] = $item; + } + } + } + + private function check() + { + if (empty($this->url) || empty($this->apiKey) || empty($this->model)) { + throw new Exception('PARAMS ERROR'); + } + } + + function chat($question = '', $config = [], &$answer_json_arr) + { + $answer = ''; + $this->curlPostChat($question, $config, function ($ch, $data) use ($question, &$answer, &$answer_json_arr) { + $answer_json_arr[] = $data; + $answer .= $data; + echo $data; + ob_flush(); + flush(); + return strlen($data); + }); + return $answer; + } + + function returnAnswer($question = '', $config = [], &$answer_json_arr) + { + $answer = ''; + $this->curlPostChat($question, $config, function ($ch, $data) use ($question, &$answer, &$answer_json_arr) { + $answer_json_arr[] = $data; + $answer .= $data; + return strlen($data); + }); + return $answer; + } + + private function curlPostChat($question = '', $config = [], $callback) + { + $url = $this->url; + $apiKey = $this->apiKey; + $model = $this->model; + $headers = ["Authorization: Bearer $apiKey", 'Accept: application/json', 'Content-Type: application/json',]; + $post_msg_body = ["model" => $model, 'stream' => true, 'chatId'=>$this->chatId]; + if ($config) { + foreach ($config as $key => $val) { + $post_msg_body[$key] = $val; + } + } + if ($question) { + $this->messages[] = ["role" => "user", "content" => $question]; + } + $post_msg_body['messages'] = $this->messages; + $postData = json_encode($post_msg_body); + $this->post_msg_body = $post_msg_body; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); + curl_setopt($ch, CURLOPT_WRITEFUNCTION, $callback); + curl_exec($ch); + } + + function getchatgptresponse($question = '', $config = []) + { + $url = $this->url; + $apiKey = $this->apiKey; + $model = $this->model; + $headers = ["Authorization: Bearer $apiKey", "Content-Type: application/json"]; + $post_msg_body = ["model" => $model, 'stream' => false]; + if ($config) { + foreach ($config as $key => $val) { + $post_msg_body[$key] = $val; + } + } + if ($question) { + $this->messages[] = ["role" => "user", "content" => $question]; + } + $post_msg_body['messages'] = $this->messages; + $post_msg_body = json_encode($post_msg_body); + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post_msg_body); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = curl_exec($ch); + if (curl_error($ch)) { + return ['code' => curl_errno($ch), 'msg' => curl_error($ch)]; + } else { + curl_close($ch); + return ['code' => 200, 'msg' => 'cURL ok', 'data' => $response]; + } + } + + private function parseData($data) + { + if (@json_decode($data)->choices[0]->message->content) { + return json_decode($data)->choices[0]->message->content; + } + $data = str_replace('data: {', '{', $data); + $data = rtrim($data, "\n\n"); + if (strpos($data, 'data: [DONE]') !== false) { + return 'data: [DONE]'; + } else { + if (false !== strpos($data, "\n\n")) { + $exp_arr = explode("\n\n", $data); + $str = ''; + try { + for ($i = 0; $i < count($exp_arr) - 1; $i++) { + $jsondecode_arr = json_decode($exp_arr[$i], true); + $str .= $jsondecode_arr['choices'][0]['delta']['content']; + } + return $str; + } catch (\Exception $e) { + return $str; + } + } + $data = @json_decode($data, true); + if (!is_array($data)) { + return ''; + } + if ($data['choices'][0]['finish_reason'] == 'stop') { + return 'data: [DONE]'; + } elseif ($data['choices'][0]['finish_reason'] == 'length') { + return 'data: [CONTINUE]'; + } + return $data['choices'][0]['delta']['content'] ?? ''; + } + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/F37A49CDED4199801B05D5FFFAF78E010.php b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/F37A49CDED4199801B05D5FFFAF78E010.php new file mode 100644 index 0000000..e373715 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/F37A49CDED4199801B05D5FFFAF78E010.php @@ -0,0 +1,141 @@ +messages[] = $item; + } + } + } + + function s3ASCD163240094B8C736D3A1A5CSD($describe = []) + { + if ($describe) { + foreach ($describe as $item) { + $this->messages[] = $item; + } + } + } + + function s326723D163240094B8C736D3A1A5CAD3($describe = []) + { + if ($describe) { + foreach ($describe as $item) { + $this->messages[] = $item; + } + } + } + + private function c2111E682402A3955B616FA5795DEDE4F() + { + if (empty($this->url) || empty($this->apiKey)) { + throw new Exception('PARAMS ERROR'); + } + } + + function cAA8AF3EBE14831A7CD1B6D1383A03755($question = '', $config = [], &$answer_json_arr) + { + $answer = ''; + $this->c8ECA4C82960B4611B076D51EF8ADF979($question, $config, function ($ch, $data) use ($question, &$answer, &$answer_json_arr) { + $answer_json_arr[] = $data; + echo $data; + ob_flush(); + flush(); + return strlen($data); + }); + } + + private function c8ECA4C82960B4611B076D51EF8ADF979($question = '', $config = [], $callback) + { + $this->c2111E682402A3955B616FA5795DEDE4F(); + $url = $this->url;; + $apiKey = $this->apiKey; + $model = $this->model; + $chatId = $this->chatId; + $detail = $this->detail; + $headers = ["Authorization: Bearer $apiKey", 'Accept: application/json', 'Content-Type: application/json',]; + $post_msg_body = ['detail' => $detail, 'stream' => true,]; + if ($detail) { + $post_msg_body['detail'] = $detail; + } + if ($chatId) { + $post_msg_body['chatId'] = $chatId; + } + if ($model) { + $post_msg_body['model'] = $model; + } + if ($config) { + foreach ($config as $key => $val) { + $post_msg_body[$key] = $val; + } + } + if ($question) { + $this->messages[] = ["role" => "user", "content" => $question]; + } + $post_msg_body['messages'] = $this->messages; + $postData = json_encode($post_msg_body); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); + curl_setopt($ch, CURLOPT_WRITEFUNCTION, $callback); + curl_exec($ch); + } + + private function pCD736318A127E12426EED8700FF2BF28($data) + { + if (@json_decode($data)->choices[0]->message->content) { + return json_decode($data)->choices[0]->message->content; + } + $data = str_replace('data: {', '{', $data); + $data = rtrim($data, "\n\n"); + if (strpos($data, 'data: [DONE]') !== false) { + return 'data: [DONE]'; + } else { + if (false !== strpos($data, "\n\n")) { + $exp_arr = explode("\n\n", $data); + $str = ''; + try { + for ($i = 0; $i < count($exp_arr) - 1; $i++) { + $jsondecode_arr = json_decode($exp_arr[$i], true); + $str .= $jsondecode_arr['choices'][0]['delta']['content']; + } + return $str; + } catch (\Exception $e) { + return $str; + } + } + $data = @json_decode($data, true); + if (!is_array($data)) { + return ''; + } + if ($data['choices'][0]['finish_reason'] == 'stop') { + return 'data: [DONE]'; + } elseif ($data['choices'][0]['finish_reason'] == 'length') { + return 'data: [CONTINUE]'; + } + return $data['choices'][0]['delta']['content'] ?? ''; + } + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/FastGPT.php b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/FastGPT.php new file mode 100644 index 0000000..737c7de --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/gpt/chat/FastGPT.php @@ -0,0 +1,148 @@ +messages[] = $item; + } + } + } + + function setBefore($describe = []) + { + if ($describe) { + foreach ($describe as $item) { + $this->messages[] = $item; + } + } + } + + function setAfter($describe = []) + { + if ($describe) { + foreach ($describe as $item) { + $this->messages[] = $item; + } + } + } + + private function check() + { + if (empty($this->url) || empty($this->apiKey)) { + throw new Exception('PARAMS ERROR'); + } + } + + function chat($question = '', $config = [], &$answer_json_arr) + { + $answer = ''; + $this->curlPostChat($question, $config, function ($ch, $data) use ($question, &$answer, &$answer_json_arr) { + $answer_json_arr[] = $data; + echo $data; + ob_flush(); + flush(); + return strlen($data); + }); + } + + private function curlPostChat($question = '', $config = [], $callback) + { + $this->check(); + $url = $this->url;; + $apiKey = $this->apiKey; + $model = $this->model; + $chatId = $this->chatId; + $detail = $this->detail; + $headers = ["Authorization: Bearer $apiKey", 'Accept: application/json', 'Content-Type: application/json',]; + $post_msg_body = ['detail' => $detail, 'stream' => true, 'chatId'=>$this->chatId]; + if ($detail) { + $post_msg_body['detail'] = $detail; + } + if ($chatId) { + $post_msg_body['chatId'] = $chatId; + } + if ($model) { + $post_msg_body['model'] = $model; + } + if ($config) { + foreach ($config as $key => $val) { + $post_msg_body[$key] = $val; + } + } + if ($question) { + $this->messages[] = ["role" => "user", "content" => $question]; + } + $post_msg_body['messages'] = $this->messages; + $postData = json_encode($post_msg_body); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); + curl_setopt($ch, CURLOPT_WRITEFUNCTION, $callback); + curl_exec($ch); + } + + private function parseData($data) + { + if (@json_decode($data)->choices[0]->message->content) { + return json_decode($data)->choices[0]->message->content; + } + $data = str_replace('data: {', '{', $data); + $data = rtrim($data, "\n\n"); + if (strpos($data, 'data: [DONE]') !== false) { + return 'data: [DONE]'; + } else { + if (false !== strpos($data, "\n\n")) { + $exp_arr = explode("\n\n", $data); + $str = ''; + try { + for ($i = 0; $i < count($exp_arr) - 1; $i++) { + $jsondecode_arr = json_decode($exp_arr[$i], true); + $str .= $jsondecode_arr['choices'][0]['delta']['content']; + } + return $str; + } catch (\Exception $e) { + return $str; + } + } + $data = @json_decode($data, true); + if (!is_array($data)) { + return ''; + } + if ($data['choices'][0]['finish_reason'] == 'stop') { + return 'data: [DONE]'; + } elseif ($data['choices'][0]['finish_reason'] == 'length') { + return 'data: [CONTINUE]'; + } + return $data['choices'][0]['delta']['content'] ?? ''; + } + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/html/Html.php b/admin/vendor/wanghua/general-utility-tools-php/src/html/Html.php new file mode 100644 index 0000000..d0ccce0 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/html/Html.php @@ -0,0 +1,224 @@ +.*?<\/a>/"; + preg_match_all($reg,$html,$a_array); + return $a_array; + } + + /** + * desc:提取div标签 + * author:wh + * @param string $html + * @return null + */ + function getDiv(string $html){ + $a_array = null; + $reg="/| .*?<\/td>/";
+ preg_match_all($reg,$html,$a_array);
+ return $a_array;
+ }
+
+
+ /**
+ * desc:提取a标签属性
+ * author:wh
+ * @param $html
+ * @return array
+ */
+ function getAAttribute($html){
+ $aarray = null;
+ $reg1="/.*?<\/a>/";
+ preg_match_all($reg1,$html,$aarray);
+
+ $hrefarray=null;//这个存放的是匹配出来的href的链接地址
+ $acontent=null;//存放匹配出来的a标签的内容
+ $reg2="/href=\"([^\"]+)/";
+ $a_arr = [];
+ for($i=0;$i 1
+ 2
+ 3
+ 4
+ 3
+ 4
+ 5 ';
+ $result = $this->get_tag_data($html,"div","class","num");
+ *
+ * author:wh
+ * @param $html
+ * @param $tag
+ * @param $class
+ * @param $value
+ * @return mixed
+ */
+ function getTagData($html,$tag,$class,$value){
+ //$value 为空,则获取class=$class的所有内容
+ $regex = $value ? "/<$tag.*?$class=\"$value\".*?>(.*?)<\/$tag>/is" : "/<$tag.*?$class=\".*?$value.*?\".*?>(.*?)<\/$tag>/is";
+ preg_match_all($regex,$html,$matches,PREG_PATTERN_ORDER);
+ //返回数组 ,指定标签内容
+ return $matches[1];
+ }
+}
\ No newline at end of file
diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/html/Htmlcontent.php b/admin/vendor/wanghua/general-utility-tools-php/src/html/Htmlcontent.php
new file mode 100644
index 0000000..5f84cff
--- /dev/null
+++ b/admin/vendor/wanghua/general-utility-tools-php/src/html/Htmlcontent.php
@@ -0,0 +1,126 @@
+ "{$url}",
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_ENCODING => "",
+ CURLOPT_MAXREDIRS => 10,
+ CURLOPT_TIMEOUT => 30,
+ CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
+ CURLOPT_CUSTOMREQUEST => "GET",
+ CURLOPT_HTTPHEADER => array(
+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
+ "Accept-Encoding: gzip, deflate, br",
+ "Accept-Language: zh-CN,zh;q=0.9",
+ "Cache-Control: no-cache",
+ "Connection: keep-alive",
+ "Pragma: no-cache",
+ "Upgrade-Insecure-Requests: 1",
+ "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
+ "cache-control: no-cache"
+ ),
+ ));
+
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+ $response = curl_exec($curl);
+ $err = curl_error($curl);
+ curl_close($curl);
+ if ($err) return false;
+ else return $response;
+ }
+
+ /**
+ * [get_tag_data 使用正则获取html内容中指定的内容]
+ * @param [type] $html [爬取的页面内容]
+ * @param [type] $tag [要查找的标签]
+ * @param [type] $attr [要查找的属性名]
+ * @param [type] $value [属性名对应的值]
+ * @return [type] [description]
+ */
+ function get_tag_data($html, $tag, $attr, $value)
+ {
+
+ // var_dump($tag, $attr, $value);
+ $regex = "/<$tag.*?$attr=\".*?$value.*?\".*?>(.*?)<\/$tag>/is";
+ // var_dump($regex);
+ preg_match_all($regex, $html, $matches, PREG_PATTERN_ORDER);
+ //var_dump($matches);die;
+ $data = isset($matches[1]) ? $matches[1] : '';
+ // var_dump($data);die;
+ return $data;
+ }
+ /**
+ * [get_html_data 使用xpath对获取到的html内容进行处理]
+ * @param [type] $html [爬取的页面内容]
+ * @param [type] $path [Xpath语句]
+ * @param integer $tag [类型 0内容 1标签内容 自定义标签]
+ * @param boolean $type [单个 还是多个(默认单个时输出单个)]
+ * @return [type] [description]
+ */
+
+ function get_html_data($html, $path, $tag = 1, $type = true)
+ {
+ $dom = new \DOMDocument();
+ @$dom->loadHTML("" . $html); // 从一个字符串加载HTML并设置UTF8编码
+ $dom->normalize(); // 使该HTML规范化
+ $xpath = new \DOMXPath($dom); //用DOMXpath加载DOM,用于查询
+ $contents = $xpath->query($path); // 获取所有内容
+ $data = [];
+ foreach ($contents as $value) {
+ if ($tag == 1) {
+ $data[] = $value->nodeValue; // 获取不带标签内容
+ } elseif ($tag == 2) {
+ $data[] = $dom->saveHtml($value); // 获取带标签内容
+ } else {
+ $data[] = $value->attributes->getNamedItem($tag)->nodeValue; // 获取attr内容
+ }
+ }
+ if (count($data) == 1) {
+ $data = $data[0];
+ }
+ return $data;
+ }
+ //调用
+ public function get_content($url,$tag,$attr='',$value='')
+ {
+ //$url = input('url') ?? '';
+ //$tag = g('tag') ?? '';
+ //$attr = g('attr') ?? '';
+ //$value = g('value') ?? '';
+ if (empty($url) || empty($tag)) {
+ return Tools::set_res(4, '缺少关键参数!');
+ }
+
+ $html = $this->curlHtml($url);
+ //使用正则获取
+ $data = $this->get_tag_data($html, $tag, $attr, $value);
+ //Xpath方法,暂未搞明白
+ // $data = $this->get_html_data($html, $path, $tag = 1, $type = true);
+ return Tools::set_res(1, 'ok!', $data);
+ }
+}
\ No newline at end of file
diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/http/Curl.php b/admin/vendor/wanghua/general-utility-tools-php/src/http/Curl.php
new file mode 100644
index 0000000..aa4f118
--- /dev/null
+++ b/admin/vendor/wanghua/general-utility-tools-php/src/http/Curl.php
@@ -0,0 +1,575 @@
+getBody()->getContents();
+ *
+ * author:wh
+ * @param $url
+ * @param string $method
+ * @param false[] $config
+ * @param array $headers
+ * @return mixed
+ */
+ static function request($url,$method='POST',$config=['verify' => false],$headers=[]){
+ $client = new Client($config);
+ //$headers = [
+ // //'User-Agent' => 'Apifox/1.0.0 (https://apifox.com)',
+ // //'Accept' => '*/*',
+ // //'Host' => 'vits_simple.excn.top',
+ // //'Connection' => 'keep-alive'
+ //];
+ $request = new Request($method, $url, $headers);
+ $res = $client->sendAsync($request)->wait();
+ //$res->getBody()->getContents();//如果是文件流,则需要用getContents()方法获取
+ return $res->getBody();
+ }
+
+ /**
+ * desc:php curl远程上传文件,提交到远程服务器,适用于上传多个文件(文件类型不限)
+ *
+ * 应用场景:
+ * 前端->后端A->后端B
+ * 说明:前端发起文件上传,后端A接收到文件后,将文件使用guzzlehttp/guzzle上传到后端B,后端B将文件处理
+ *
+ * 当前版本依赖:"guzzlehttp/guzzle": "^7.8"
+ *
+ * author:wh
+ * @param $url 远程地址
+ * @param $params 提交参数
+ * @param array $config 配置项
+ */
+ function curlRemoteUploadFiles($url,$params, $config=['verify' => false]){
+ // 创建 Guzzle HTTP 客户端实例
+ $client = new Client($config);
+
+ // 定义要上传的文件列表
+ $files = [
+ //[
+ // 'name' => 'file1', // 服务器接收的文件字段名
+ // 'contents' => Utils::tryFopen('D:\wanghua\projects\big_world_projects\universal_gravitation\public\uploads\20240506\dc1c441d81d48565ac6817b89d0f8bef.mp4', 'r'), // 文件路径
+ // 'filename' => 'dc1c441d81d48565ac6817b89d0f8bef.mp4', // 文件名,可自定义
+ //],
+ //[
+ // 'name' => 'files',
+ // 'contents' => Utils::tryFopen('D:\wanghua\projects\big_world_projects\universal_gravitation\public\uploads\20240506\f80179b23b60619fba8033bfd64f8817.mp4', 'r'),
+ // 'filename' => 'f80179b23b60619fba8033bfd64f8817.mp4',
+ //],
+ //['name'=>'prompt','contents'=>$prompt],
+ //['name'=>'tts_url','contents'=>$tts_url]
+ // 可以继续添加更多文件...
+ ];
+ foreach ($params as $key=>$val){
+ $files[] = ['name'=>$key,'contents'=>$val];
+ }
+ $controller = \request()->controller();
+ $action = \request()->action();
+ $save_path = Tools::get_root_path() . "public/uploads/{$controller}/{$action}/";
+ $files_obj = \request()->file('files');
+ foreach ($files_obj as $k=>$file) {
+ if ($file->check()) {
+ $save_res = $file->move($save_path);
+ $fileinfo = $file->getInfo();
+ // 使用CURLFile包装文件路径以供上传
+ $filename = $save_path.date('Ymd').'/'.$save_res->getFilename();
+ $files[] = [
+ 'name'=>"files",//服务器接收的文件字段名
+ 'contents'=>Utils::tryFopen($filename, 'r'),
+ 'filename'=>$fileinfo['name'],//可自定义
+ ];
+ }
+ //else {
+ // return json(['code' => -1, 'msg' => '文件上传失败: ' . $file->getError()]);
+ //}
+ }
+
+ // 构建多部分表单数据
+ $multipartStream = new MultipartStream($files);
+ $boundary = $multipartStream->getBoundary();
+ $headers = [
+ 'Content-Type' => "multipart/form-data; boundary={$boundary}",
+ ];
+
+ // 准备请求体
+ $body = $multipartStream->getContents();
+
+ // 发起 POST 请求
+ try {
+ $response = $client->request('POST', $url, [
+ 'headers' => $headers,
+ 'body' => $body,
+ ]);
+
+ // 处理响应
+ $responseBody = (string) $response->getBody();
+ //echo "Server response: " . $responseBody;
+ return Tools::set_ok('ok',$responseBody);
+ } catch (\GuzzleHttp\Exception\RequestException $e) {
+ // 错误处理
+ //echo "Error: " . $e->getMessage();
+ return Tools::set_fail("Error: " . $e->getMessage());
+ }
+ }
+
+ /**
+ * @deprecated 弃用
+ * GET
+ *
+ * author:wh
+ * @param string $url
+ * @param int $timeout
+ * @return bool|int|string
+ */
+ static function curl_get(string $url, int $timeout = 10, $header=[])
+ {
+
+ $header = $header?:array(
+ 'Accept: application/json',
+ );
+ $curl = curl_init();
+ //curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
+ //curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
+ //curl_setopt($curl, CURLOPT_SSLVERSION, 3);
+ //设置抓取的url
+ curl_setopt($curl, CURLOPT_URL, $url);
+ //设置头文件的信息作为数据流输出
+ curl_setopt($curl, CURLOPT_HEADER, 0);
+ // 超时设置,以秒为单位
+ curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
+
+ // 超时设置,以毫秒为单位
+ // curl_setopt($curl, CURLOPT_TIMEOUT_MS, 500);
+
+ // 设置请求头
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
+ //设置获取的信息以文件流的形式返回,而不是直接输出。
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+ //执行命令
+ $data = curl_exec($curl);
+
+ // 显示错误信息
+ if (curl_error($curl)) {
+ //print "Error: ".curl_errno($curl).'-' . curl_error($curl);
+ //返回错误码
+ return ['code' => curl_errno($curl), 'msg' => curl_error($curl)];
+ } else {
+ //关闭句柄
+ curl_close($curl);
+ // 返回的内容
+ return ['code' => 200, 'msg' => 'cURL ok', 'data' => $data];
+ }
+ }
+ /**
+ * @deprecated 弃用
+ * POST 表单
+ *
+ * author:wh
+ * @param string $url 是请求的链接
+ * @param $postdata 传输的数据,数组格式 (跟$header的参数相关)
+ * @return bool|int|string
+ */
+ static function curl_post(string $url, $postdata, $header=[]) {
+ $timeout = 4;
+ $connect_timeout = 1;
+ $set_time_limit = 5;
+ if($timeout + $connect_timeout < $set_time_limit) throw new \Exception('脚本超时值必须大于等于连接超时与请求处理超时之和');
+ set_time_limit($set_time_limit);
+ $header = $header?:array(
+ 'Accept: application/json',
+ );
+
+ //初始化
+ $curl = curl_init();
+ //设置抓取的url
+ curl_setopt($curl, CURLOPT_URL, $url);
+ //设置头文件的信息作为数据流输出
+ curl_setopt($curl, CURLOPT_HEADER, 0);
+ //设置获取的信息以文件流的形式返回,而不是直接输出。
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ // 超时设置
+ curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
+ //发起连接前等待的时间,如果设置为0,则无限等待。
+ curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $connect_timeout);
+
+ // 超时设置,以毫秒为单位
+ // curl_setopt($curl, CURLOPT_TIMEOUT_MS, 500);
+
+ // 设置请求头
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
+
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE );
+ curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE );
+
+ //设置post方式提交
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $postdata);
+ //执行命令
+ $data = curl_exec($curl);
+
+ // 显示错误信息
+ if (curl_error($curl)) {
+ //返回错误码
+ return ['code'=>curl_errno($curl), 'msg'=>curl_error($curl)];
+ } else {
+ //关闭句柄
+ curl_close($curl);
+ // 返回的内容
+ return ['code'=>200, 'msg'=>'cURL ok', 'data'=>$data];
+ }
+ }
+
+ /**
+ * @deprecated 弃用
+ * POST 查询参数
+ *
+ * author:wh
+ * @param string $url 是请求的链接
+ * @param array $postdata 传输的数据,http_build_query查询参数格式
+ * @return bool|int|string
+ */
+ static function curl_post_build_query(string $url, $postdata, $header=[]) {
+ $timeout = 4;
+ $connect_timeout = 1;
+ $set_time_limit = 5;
+ if($timeout + $connect_timeout < $set_time_limit) throw new \Exception('脚本超时值必须大于等于连接超时与请求处理超时之和');
+ set_time_limit($set_time_limit);
+ $header = $header?:array(
+ 'Accept: application/json',
+ );
+
+ //初始化
+ $curl = curl_init();
+ //设置抓取的url
+ curl_setopt($curl, CURLOPT_URL, $url);
+ //设置头文件的信息作为数据流输出
+ curl_setopt($curl, CURLOPT_HEADER, 0);
+ //设置获取的信息以文件流的形式返回,而不是直接输出。
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ // 超时设置
+ curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
+ //发起连接前等待的时间,如果设置为0,则无限等待。
+ curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $connect_timeout);
+
+ // 超时设置,以毫秒为单位
+ // curl_setopt($curl, CURLOPT_TIMEOUT_MS, 500);
+
+ // 设置请求头
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
+
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE );
+ curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE );
+
+ //设置post方式提交
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($postdata));
+ //执行命令
+ $data = curl_exec($curl);
+
+ // 显示错误信息
+ if (curl_error($curl)) {
+ //返回错误码
+ return ['code'=>curl_errno($curl), 'msg'=>curl_error($curl)];
+ } else {
+ //关闭句柄
+ curl_close($curl);
+ // 返回的内容
+ return ['code'=>200, 'msg'=>'ok', 'data'=>$data];
+ }
+ }
+
+ /**
+ * @deprecated 弃用
+ * POST json
+ *
+ * author:wh
+ * @param string $url 是请求的链接
+ * @param array $postdata 传输的数据,json格式
+ * @return 返回数组格式
+ */
+ static function curl_post_json(string $url, $postdata, $header=[]) {
+ $timeout = 4;
+ $connect_timeout = 1;
+ $set_time_limit = 5;
+ if($timeout + $connect_timeout < $set_time_limit) throw new \Exception('脚本超时值必须大于等于连接超时与请求处理超时之和');
+ set_time_limit($set_time_limit);
+ $header = $header?:array(
+ 'Accept: application/json',
+ );
+
+ //初始化
+ $curl = curl_init();
+ //设置抓取的url
+ curl_setopt($curl, CURLOPT_URL, $url);
+ //设置头文件的信息作为数据流输出
+ curl_setopt($curl, CURLOPT_HEADER, 0);
+ //设置获取的信息以文件流的形式返回,而不是直接输出。
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ // 超时设置
+ curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
+ //发起连接前等待的时间,如果设置为0,则无限等待。
+ curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $connect_timeout);
+
+ // 超时设置,以毫秒为单位
+ // curl_setopt($curl, CURLOPT_TIMEOUT_MS, 500);
+
+ // 设置请求头
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
+
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE );
+ curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE );
+
+ //设置post方式提交
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($postdata,JSON_UNESCAPED_UNICODE));
+ //执行命令
+ $data = curl_exec($curl);
+
+ // 显示错误信息
+ if (curl_error($curl)) {
+ //返回错误码
+ return ['code'=>curl_errno($curl), 'msg'=>curl_error($curl)];
+ } else {
+ //关闭句柄
+ curl_close($curl);
+ // 返回的内容
+ return ['code'=>200, 'msg'=>'cURL ok', 'data'=>$data];
+ }
+ }
+
+ /**
+ * @deprecated 弃用
+ * POST json ,成功直接返回请求结果,错误返回数组
+ *
+ * 注:此方法不是表单提交
+ *
+ * author:wh
+ * @param string $url 是请求的链接
+ * @param array $postdata 传输的数据,最终会转换为json格式请求
+ * @return 直接返回请求数据,适合请求第三方返回BUFFER数据流的接口
+ */
+ static function curl_post_json_return_buffer(string $url, array $postdata, $header=[]) {
+ $timeout = 4;
+ $connect_timeout = 1;
+ $set_time_limit = 5;
+ if($timeout + $connect_timeout < $set_time_limit) throw new \Exception('脚本超时值必须大于等于连接超时与请求处理超时之和');
+ set_time_limit($set_time_limit);
+ $header = $header?:array(
+ 'Accept: application/json',
+ );
+
+ //初始化
+ $curl = curl_init();
+ //设置抓取的url
+ curl_setopt($curl, CURLOPT_URL, $url);
+ //设置头文件的信息作为数据流输出
+ curl_setopt($curl, CURLOPT_HEADER, 0);
+ //设置获取的信息以文件流的形式返回,而不是直接输出。
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ // 超时设置
+ curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
+ //发起连接前等待的时间,如果设置为0,则无限等待。
+ curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $connect_timeout);
+
+ // 超时设置,以毫秒为单位
+ // curl_setopt($curl, CURLOPT_TIMEOUT_MS, 500);
+
+ // 设置请求头
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
+
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE );
+ curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE );
+
+ //设置post方式提交
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($postdata,JSON_UNESCAPED_UNICODE));
+ //执行命令
+ $data = curl_exec($curl);
+
+ // 显示错误信息
+ if (curl_error($curl)) {
+ //返回错误码
+ return ['code'=>curl_errno($curl), 'msg'=>curl_error($curl)];
+ } else {
+ //关闭句柄
+ curl_close($curl);
+ // 返回的内容
+ return $data;
+ }
+ }
+
+ /**
+ * @deprecated 弃用
+ * POST [流]
+ *
+ * [请求Java接口]
+ *
+ * 思考:1、Java端如果用文件流的方式去获取数据,调用此方法
+ *
+ * @param $url
+ * @param $postdata
+ * @param int $timeout
+ * @return array
+ * @link
+ * @example
+ * @see
+ */
+ static function java_curl_post_file_request($url, $postdata, $timeout = 10, $header=[])
+ {
+
+ $header = $header?:array(
+ 'Accept: application/json',
+ );
+
+ //初始化
+ $curl = curl_init();
+ //设置抓取的url
+ curl_setopt($curl, CURLOPT_URL, $url);
+ //设置头文件的信息作为数据流输出
+ curl_setopt($curl, CURLOPT_HEADER, 0);
+ //设置获取的信息以文件流的形式返回,而不是直接输出。
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+ // 超时设置
+ curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
+
+ // 超时设置,以毫秒为单位
+ // curl_setopt($curl, CURLOPT_TIMEOUT_MS, 500);
+
+ // 设置请求头
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
+
+ curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
+ curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
+
+ //设置post方式提交
+ curl_setopt($curl, CURLOPT_POST, 1);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $postdata);
+ //执行命令
+ $data = curl_exec($curl);
+
+ // 显示错误信息
+ if (curl_error($curl)) {
+ //返回错误码
+ return ['code' => curl_errno($curl), 'msg' => curl_error($curl)];
+ } else {
+ //关闭句柄
+ curl_close($curl);
+ // 返回的内容
+ return ['code' => 200, 'msg' => 'ok', 'data' => $data];
+ }
+ }
+
+ /**
+ * @deprecated 弃用
+ * 统一请求 GET/POST请求
+ *
+ * 注:此设置允许header重定向,适合部分请求头中携带参数的接口,如请求头携带token
+ *
+ * @param String $url 接口地址
+ */
+ static function curl_request($url, $method = 'GET',$data=null,$header=array(),$call_back=null)
+ {
+ set_time_limit(30);
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ if($header){
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
+ }
+ if($method == 'POST'){
+ if($data) curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+ }
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+ if($call_back){
+ //使用此特性前除非你清楚理解回调函数,否则不推荐
+ curl_setopt($ch, CURLOPT_WRITEFUNCTION, $call_back);
+ }
+ $result = curl_exec($ch);
+ if (curl_errno($ch)) {
+ return [
+ 'status' => 'error',
+ 'message' => 'curl 错误信息: ' . curl_error($ch)
+ ];
+ }
+ curl_close($ch);
+ return $result;
+ }
+
+
+ /**
+ * @deprecated 弃用
+ * desc:php curl模拟文件上传
+ * post
+ * author:wh
+ * @param $url 提交地址
+ * @param $params 表单参数
+ * @param $files 上传文件
+ * @param $header 请求头
+ * @return array
+ */
+ static function curlFileUpload($url,$params,$files,$header)
+ {
+ // 初始化cURL会话
+ $ch = curl_init();
+ $header = $header ?: array(
+ 'Accept: application/json',
+ 'Content-Type: multipart/form-data'
+ );
+ // 合并文件和其他字段数据
+ $data = array_merge($params, $files);
+
+ // 设置cURL选项
+ curl_setopt($ch, CURLOPT_URL, $url);
+ // 设置请求头
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
+
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
+
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+
+ // 执行cURL请求
+ $result = curl_exec($ch);
+ if (curl_errno($ch)) {
+ return ['code' => curl_errno($ch), 'msg' => curl_error($ch)];
+ }
+ //关闭句柄
+ curl_close($ch);
+ // 返回的内容
+ return ['code' => 200, 'msg' => 'cURL ok', 'data' => $result];
+ }
+
+}
\ No newline at end of file
diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/image/Image.php b/admin/vendor/wanghua/general-utility-tools-php/src/image/Image.php
new file mode 100644
index 0000000..ecac61e
--- /dev/null
+++ b/admin/vendor/wanghua/general-utility-tools-php/src/image/Image.php
@@ -0,0 +1,76 @@
+这是一张图片:点击查看
+//还有一张: 图片链接也可以是: \n"; + } +} diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/Mail.php b/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/Mail.php new file mode 100644 index 0000000..a420dd8 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/Mail.php @@ -0,0 +1,220 @@ +nickname = $nickname;//发送方昵称 + $this->host = $host;//发送方邮件服务器地址 + $this->username = $username;//发送方邮件服务器账号 + $this->pwd = $pwd;//发送方邮件服务器密码 + $this->sender_email = $sender_email;//发送方邮箱 + $this->port = $port;//发送方邮件服务器端口 有465, 25 2465等 + $this->protocol = $protocol;//发送方邮件服务器要求的协议 有ssl、tls + } + + /** + * @deprecated [老项目-发送国内邮箱] + * + * 注意:发送人和发送人邮箱一致才能用此方法,否则发送失败 + * @Author + * @Date 2019-11-22 + * @param [array|string] $receiver [收件人邮箱] + * @param [array|string] $receiver_nick [收件人名称] + * @param [string] $subject [邮件主题] + * @param [string] $body [邮件正文] + * @return [type] [description] + */ + function send($receiver,$receiver_nick,$subject,$body){ + if(empty($receiver)){ + return ['code'=>500, 'msg'=>'收件人邮箱为空']; + } + + $email_nickname = $this->nickname;//平台邮箱名称 + + $mail = new PHPMailer(); + $mail->isSMTP(); + if($this->debug){ + $mail->SMTPDebug = $this->debug; // 调试模式 + } + $mail->Host = $this->host; // 邮件服务器地址 + $mail->SMTPAuth = true; // 允许 SMTP 认证 + $mail->CharSet = "UTF-8"; // 编码格式 + $mail->Username = $this->username; // SMTP 用户名 即邮箱的用户名 + $mail->Password = $this->pwd; // SMTP 密码 + $mail->SMTPSecure = $this->protocol; // TLS或者ssl协议 + $mail->Port = $this->port; // 服务器端口 25 或者465 具体要看邮箱服务器支持 + $mail->setFrom($this->username, $email_nickname); + //dump($mail);die; + if(is_array($receiver)){ + //多个收件人 + foreach ($receiver as $key => $val){ + $mail->addAddress($val, $receiver_nick[$key]); + } + }else{ + //单个收件人 + $mail->addAddress($receiver, $receiver_nick); + } + + $mail->isHTML(true); //是否以HTML文档格式发送 + $mail->Subject = $subject; + $mail->Body = $body; //发送带有html标签文本 + // $mail->AltBody = ''; //如果邮件客户端不支持HTML则显示此内容 + // $mail->addCC(); // 添加抄送 + // $mail->addBCC(); // 添加密送 + // $mail->addAttachment("2e3f19fd-8e3f-405b-9854-41dd19ddebf8.jpg"); // 添加附件 + + if(!$mail->send()){ + return ['code'=>500, 'msg'=>$mail->ErrorInfo]; + } + return ['code'=>200, 'msg'=>'Email has been sent.']; + } + + + /** + * [通用发送国内邮箱] + * + * 注意:发送人和发送人邮箱一致才能用此方法,否则发送失败 + * @Author + * @Date 2019-11-22 + * @param [array|string] $receiver [收件人邮箱] + * @param [array|string] $receiver_nick [收件人名称] + * @param [string] $subject [邮件主题] + * @param [string] $body [邮件正文] + * @return [type] [description] + */ + function sendInner($receiver,$receiver_nick,$subject,$body){ + if(empty($receiver)){ + return ['code'=>500, 'msg'=>'收件人邮箱为空']; + } + + $email_nickname = $this->nickname;//平台邮箱名称 + + $mail = new PHPMailer(); + $mail->isSMTP(); + if($this->debug){ + $mail->SMTPDebug = $this->debug; // 调试模式 + } + $mail->Host = $this->host; // 邮件服务器地址 + $mail->SMTPAuth = true; // 允许 SMTP 认证 + $mail->CharSet = "UTF-8"; // 编码格式 + $mail->Username = $this->username; // SMTP 用户名 即邮箱的用户名 + $mail->Password = $this->pwd; // SMTP 密码 + $mail->SMTPSecure = $this->protocol; // TLS或者ssl协议 + $mail->Port = $this->port; // 服务器端口 25 或者465 具体要看邮箱服务器支持 + $mail->setFrom($this->sender_email, $email_nickname); + if(is_array($receiver)){ + //多个收件人 + foreach ($receiver as $key => $val){ + $mail->addAddress($val, $receiver_nick[$key]); + } + }else{ + //单个收件人 + $mail->addAddress($receiver, $receiver_nick); + } + + $mail->isHTML(true); //是否以HTML文档格式发送 + $mail->Subject = $subject; + $mail->Body = $body; //发送带有html标签文本 + // $mail->AltBody = ''; //如果邮件客户端不支持HTML则显示此内容 + // $mail->addCC(); // 添加抄送 + // $mail->addBCC(); // 添加密送 + // $mail->addAttachment("2e3f19fd-8e3f-405b-9854-41dd19ddebf8.jpg"); // 添加附件 + + if(!$mail->send()){ + return ['code'=>500, 'msg'=>$mail->ErrorInfo]; + } + return ['code'=>200, 'msg'=>'Email has been sent.']; + } + + /** + * [发送【国外】aws亚马逊邮件] + * + * 邮件服务器用亚马逊服务器,接收者邮箱不限制 + * + * 注意: + * 发送方要在亚马逊控制台,添加发送者邮箱,才可以对外发邮件 + * 端口用25,25测试通过 + * + * @Author + * @Date 2019-11-22 + * @param [array|string] $sender [发件人邮箱] + * @param [array|string] $receiver [收件人邮箱] + * @param [array|string] $receiver_nick [收件人名称] + * @param [string] $subject [邮件主题] + * @param [string] $body [邮件正文] + * @return [type] [description] + */ + function sendAWS($receiver,$receiver_nick,$subject,$body){ + if(empty($receiver)){ + return ['code'=>500, 'msg'=>'收件人邮箱为空']; + } + if(empty($this->sender_email)){ + return ['code'=>500, 'msg'=>'发件人邮箱为空']; + } + + $email_nickname = $this->nickname;//平台邮箱名称 + + $mail = new PHPMailer(); + $mail->isSMTP(); + if($this->debug){ + $mail->SMTPDebug = $this->debug; // 调试模式 + } + //$mail->setFrom($this->sender_email, $email_nickname); + + $mail->Host = $this->host; // 邮件服务器地址 + $mail->SMTPAuth = true; // 允许 SMTP 认证 + $mail->CharSet = "UTF-8"; // 编码格式 + $mail->Username = $this->username; // SMTP 用户名 即邮箱的用户名 + $mail->Password = $this->pwd; // SMTP 密码 + $mail->SMTPSecure = $this->protocol; // TLS或者ssl协议 + $mail->Port = $this->port; // 服务器端口 腾讯465,具体要看邮箱服务器支持 亚马逊25 或 2465 + $mail->setFrom($this->sender_email, $email_nickname); + + if(is_array($receiver)){ + //多个收件人 + foreach ($receiver as $key => $val){ + $mail->addAddress($val, $receiver_nick[$key]); + } + }else{ + //单个收件人 + $mail->addAddress($receiver, $receiver_nick); + } + + $mail->isHTML(true); //是否以HTML文档格式发送 + $mail->Subject = $subject; + $mail->Body = $body; //发送带有html标签文本 + // $mail->AltBody = ''; //如果邮件客户端不支持HTML则显示此内容 + // $mail->addCC(); // 添加抄送 + // $mail->addBCC(); // 添加密送 + // $mail->addAttachment("2e3f19fd-8e3f-405b-9854-41dd19ddebf8.jpg"); // 添加附件 + + if(!$mail->send()){ + return ['code'=>500, 'msg'=>$mail->ErrorInfo]; + } + return ['code'=>200, 'msg'=>'Email has been sent.']; + } + +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/PHPMailer.php b/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/PHPMailer.php new file mode 100644 index 0000000..90baa6f --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/PHPMailer.php @@ -0,0 +1,4770 @@ + + * @author Jim Jagielski (jimjag) `, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise. + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * ```php + * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * ``` + * + * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` + * level output is used: + * + * ```php + * $mail->Debugoutput = new myPsr3Logger; + * ``` + * + * @see SMTP::$Debugoutput + * + * @var string|callable|\Psr\Log\LoggerInterface + */ + public $Debugoutput = 'echo'; + + /** + * Whether to keep SMTP connection open after each message. + * If this is set to true then to close the connection + * requires an explicit call to smtpClose(). + * + * @var bool + */ + public $SMTPKeepAlive = false; + + /** + * Whether to split multiple to addresses into multiple messages + * or send them all in one message. + * Only supported in `mail` and `sendmail` transports, not in SMTP. + * + * @var bool + */ + public $SingleTo = false; + + /** + * Storage for addresses when SingleTo is enabled. + * + * @var array + */ + protected $SingleToArray = []; + + /** + * Whether to generate VERP addresses on send. + * Only applicable when sending via SMTP. + * + * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path + * @see http://www.postfix.org/VERP_README.html Postfix VERP info + * + * @var bool + */ + public $do_verp = false; + + /** + * Whether to allow sending messages with an empty body. + * + * @var bool + */ + public $AllowEmpty = false; + + /** + * DKIM selector. + * + * @var string + */ + public $DKIM_selector = ''; + + /** + * DKIM Identity. + * Usually the email address used as the source of the email. + * + * @var string + */ + public $DKIM_identity = ''; + + /** + * DKIM passphrase. + * Used if your key is encrypted. + * + * @var string + */ + public $DKIM_passphrase = ''; + + /** + * DKIM signing domain name. + * + * @example 'example.com' + * + * @var string + */ + public $DKIM_domain = ''; + + /** + * DKIM Copy header field values for diagnostic use. + * + * @var bool + */ + public $DKIM_copyHeaderFields = true; + + /** + * DKIM Extra signing headers. + * + * @example ['List-Unsubscribe', 'List-Help'] + * + * @var array + */ + public $DKIM_extraHeaders = []; + + /** + * DKIM private key file path. + * + * @var string + */ + public $DKIM_private = ''; + + /** + * DKIM private key string. + * + * If set, takes precedence over `$DKIM_private`. + * + * @var string + */ + public $DKIM_private_string = ''; + + /** + * Callback Action function name. + * + * The function that handles the result of the send email action. + * It is called out by send() for each email sent. + * + * Value can be any php callable: http://www.php.net/is_callable + * + * Parameters: + * bool $result result of the send action + * array $to email addresses of the recipients + * array $cc cc email addresses + * array $bcc bcc email addresses + * string $subject the subject + * string $body the email body + * string $from email address of sender + * string $extra extra information of possible use + * "smtp_transaction_id' => last smtp transaction id + * + * @var string + */ + public $action_function = ''; + + /** + * What to put in the X-Mailer header. + * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use. + * + * @var string|null + */ + public $XMailer = ''; + + /** + * Which validator to use by default when validating email addresses. + * May be a callable to inject your own validator, but there are several built-in validators. + * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option. + * + * @see PHPMailer::validateAddress() + * + * @var string|callable + */ + public static $validator = 'php'; + + /** + * An instance of the SMTP sender class. + * + * @var SMTP + */ + protected $smtp; + + /** + * The array of 'to' names and addresses. + * + * @var array + */ + protected $to = []; + + /** + * The array of 'cc' names and addresses. + * + * @var array + */ + protected $cc = []; + + /** + * The array of 'bcc' names and addresses. + * + * @var array + */ + protected $bcc = []; + + /** + * The array of reply-to names and addresses. + * + * @var array + */ + protected $ReplyTo = []; + + /** + * An array of all kinds of addresses. + * Includes all of $to, $cc, $bcc. + * + * @see PHPMailer::$to + * @see PHPMailer::$cc + * @see PHPMailer::$bcc + * + * @var array + */ + protected $all_recipients = []; + + /** + * An array of names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $all_recipients + * and one of $to, $cc, or $bcc. + * This array is used only for addresses with IDN. + * + * @see PHPMailer::$to + * @see PHPMailer::$cc + * @see PHPMailer::$bcc + * @see PHPMailer::$all_recipients + * + * @var array + */ + protected $RecipientsQueue = []; + + /** + * An array of reply-to names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $ReplyTo. + * This array is used only for addresses with IDN. + * + * @see PHPMailer::$ReplyTo + * + * @var array + */ + protected $ReplyToQueue = []; + + /** + * The array of attachments. + * + * @var array + */ + protected $attachment = []; + + /** + * The array of custom headers. + * + * @var array + */ + protected $CustomHeader = []; + + /** + * The most recent Message-ID (including angular brackets). + * + * @var string + */ + protected $lastMessageID = ''; + + /** + * The message's MIME type. + * + * @var string + */ + protected $message_type = ''; + + /** + * The array of MIME boundary strings. + * + * @var array + */ + protected $boundary = []; + + /** + * The array of available languages. + * + * @var array + */ + protected $language = []; + + /** + * The number of errors encountered. + * + * @var int + */ + protected $error_count = 0; + + /** + * The S/MIME certificate file path. + * + * @var string + */ + protected $sign_cert_file = ''; + + /** + * The S/MIME key file path. + * + * @var string + */ + protected $sign_key_file = ''; + + /** + * The optional S/MIME extra certificates ("CA Chain") file path. + * + * @var string + */ + protected $sign_extracerts_file = ''; + + /** + * The S/MIME password for the key. + * Used only if the key is encrypted. + * + * @var string + */ + protected $sign_key_pass = ''; + + /** + * Whether to throw exceptions for errors. + * + * @var bool + */ + protected $exceptions = false; + + /** + * Unique ID used for message ID and boundaries. + * + * @var string + */ + protected $uniqueid = ''; + + /** + * The PHPMailer Version number. + * + * @var string + */ + const VERSION = '6.1.3'; + + /** + * Error severity: message only, continue processing. + * + * @var int + */ + const STOP_MESSAGE = 0; + + /** + * Error severity: message, likely ok to continue processing. + * + * @var int + */ + const STOP_CONTINUE = 1; + + /** + * Error severity: message, plus full stop, critical error reached. + * + * @var int + */ + const STOP_CRITICAL = 2; + + /** + * SMTP RFC standard line ending. + * + * @var string + */ + protected static $LE = "\r\n"; + + /** + * The maximum line length supported by mail(). + * + * Background: mail() will sometimes corrupt messages + * with headers headers longer than 65 chars, see #818. + * + * @var int + */ + const MAIL_MAX_LINE_LENGTH = 63; + + /** + * The maximum line length allowed by RFC 2822 section 2.1.1. + * + * @var int + */ + const MAX_LINE_LENGTH = 998; + + /** + * The lower maximum line length allowed by RFC 2822 section 2.1.1. + * This length does NOT include the line break + * 76 means that lines will be 77 or 78 chars depending on whether + * the line break format is LF or CRLF; both are valid. + * + * @var int + */ + const STD_LINE_LENGTH = 76; + + /** + * Constructor. + * + * @param bool $exceptions Should we throw external exceptions? + */ + public function __construct($exceptions = null) + { + if (null !== $exceptions) { + $this->exceptions = (bool) $exceptions; + } + //Pick an appropriate debug output format automatically + $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html'); + } + + /** + * Destructor. + */ + public function __destruct() + { + //Close any open SMTP connection nicely + $this->smtpClose(); + } + + /** + * Call mail() in a safe_mode-aware fashion. + * Also, unless sendmail_path points to sendmail (or something that + * claims to be sendmail), don't pass params (not a perfect fix, + * but it will do). + * + * @param string $to To + * @param string $subject Subject + * @param string $body Message Body + * @param string $header Additional Header(s) + * @param string|null $params Params + * + * @return bool + */ + private function mailPassthru($to, $subject, $body, $header, $params) + { + //Check overloading of mail function to avoid double-encoding + if (ini_get('mbstring.func_overload') & 1) { + $subject = $this->secureHeader($subject); + } else { + $subject = $this->encodeHeader($this->secureHeader($subject)); + } + //Calling mail() with null params breaks + if (!$this->UseSendmailOptions || null === $params) { + $result = @mail($to, $subject, $body, $header); + } else { + $result = @mail($to, $subject, $body, $header, $params); + } + + return $result; + } + + /** + * Output debugging info via user-defined method. + * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug). + * + * @see PHPMailer::$Debugoutput + * @see PHPMailer::$SMTPDebug + * + * @param string $str + */ + protected function edebug($str) + { + if ($this->SMTPDebug <= 0) { + return; + } + //Is this a PSR-3 logger? + if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { + $this->Debugoutput->debug($str); + + return; + } + //Avoid clash with built-in function names + if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) { + call_user_func($this->Debugoutput, $str, $this->SMTPDebug); + + return; + } + switch ($this->Debugoutput) { + case 'error_log': + //Don't output, just log + error_log($str); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ), " \n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace('/\r\n|\r/m', "\n", $str); + echo gmdate('Y-m-d H:i:s'), + "\t", + //Trim trailing space + trim( + //Indent for readability, except for trailing break + str_replace( + "\n", + "\n \t ", + trim($str) + ) + ), + "\n"; + } + } + + /** + * Sets message type to HTML or plain. + * + * @param bool $isHtml True for HTML mode + */ + public function isHTML($isHtml = true) + { + if ($isHtml) { + $this->ContentType = static::CONTENT_TYPE_TEXT_HTML; + } else { + $this->ContentType = static::CONTENT_TYPE_PLAINTEXT; + } + } + + /** + * Send messages using SMTP. + */ + public function isSMTP() + { + $this->Mailer = 'smtp'; + } + + /** + * Send messages using PHP's mail() function. + */ + public function isMail() + { + $this->Mailer = 'mail'; + } + + /** + * Send messages using $Sendmail. + */ + public function isSendmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (false === stripos($ini_sendmail_path, 'sendmail')) { + $this->Sendmail = '/usr/sbin/sendmail'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'sendmail'; + } + + /** + * Send messages using qmail. + */ + public function isQmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (false === stripos($ini_sendmail_path, 'qmail')) { + $this->Sendmail = '/var/qmail/bin/qmail-inject'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'qmail'; + } + + /** + * Add a "To" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addAddress($address, $name = '') + { + return $this->addOrEnqueueAnAddress('to', $address, $name); + } + + /** + * Add a "CC" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('cc', $address, $name); + } + + /** + * Add a "BCC" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addBCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('bcc', $address, $name); + } + + /** + * Add a "Reply-To" address. + * + * @param string $address The email address to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + public function addReplyTo($address, $name = '') + { + return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer + * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still + * be modified after calling this function), addition of such addresses is delayed until send(). + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + protected function addOrEnqueueAnAddress($kind, $address, $name) + { + $address = trim($address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + $pos = strrpos($address, '@'); + if (false === $pos) { + // At-sign is missing. + $error_message = sprintf( + '%s (%s): %s', + $this->lang('invalid_address'), + $kind, + $address + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + $params = [$kind, $address, $name]; + // Enqueue addresses with IDN until we know the PHPMailer::$CharSet. + if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) { + if ('Reply-To' !== $kind) { + if (!array_key_exists($address, $this->RecipientsQueue)) { + $this->RecipientsQueue[$address] = $params; + + return true; + } + } elseif (!array_key_exists($address, $this->ReplyToQueue)) { + $this->ReplyToQueue[$address] = $params; + + return true; + } + + return false; + } + + // Immediately add standard addresses without IDN. + return call_user_func_array([$this, 'addAnAddress'], $params); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * + * @throws Exception + * + * @return bool true on success, false if address already used or invalid in some way + */ + protected function addAnAddress($kind, $address, $name = '') + { + if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { + $error_message = sprintf( + '%s: %s', + $this->lang('Invalid recipient kind'), + $kind + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if (!static::validateAddress($address)) { + $error_message = sprintf( + '%s (%s): %s', + $this->lang('invalid_address'), + $kind, + $address + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + if ('Reply-To' !== $kind) { + if (!array_key_exists(strtolower($address), $this->all_recipients)) { + $this->{$kind}[] = [$address, $name]; + $this->all_recipients[strtolower($address)] = true; + + return true; + } + } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) { + $this->ReplyTo[strtolower($address)] = [$address, $name]; + + return true; + } + + return false; + } + + /** + * Parse and validate a string containing one or more RFC822-style comma-separated email addresses + * of the form "display name " into an array of name/address pairs. + * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. + * Note that quotes in the name part are removed. + * + * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation + * + * @param string $addrstr The address list string + * @param bool $useimap Whether to use the IMAP extension to parse the list + * + * @return array + */ + public static function parseAddresses($addrstr, $useimap = true) + { + $addresses = []; + if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { + //Use this built-in parser if it's available + $list = imap_rfc822_parse_adrlist($addrstr, ''); + foreach ($list as $address) { + if (('.SYNTAX-ERROR.' !== $address->host) && static::validateAddress( + $address->mailbox . '@' . $address->host + )) { + $addresses[] = [ + 'name' => (property_exists($address, 'personal') ? $address->personal : ''), + 'address' => $address->mailbox . '@' . $address->host, + ]; + } + } + } else { + //Use this simpler parser + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if (static::validateAddress($address)) { + $addresses[] = [ + 'name' => '', + 'address' => $address, + ]; + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + if (static::validateAddress($email)) { + $addresses[] = [ + 'name' => trim(str_replace(['"', "'"], '', $name)), + 'address' => $email, + ]; + } + } + } + } + + return $addresses; + } + + /** + * Set the From and FromName properties. + * + * @param string $address + * @param string $name + * @param bool $auto Whether to also set the Sender address, defaults to true + * + * @throws Exception + * + * @return bool + */ + public function setFrom($address, $name = '', $auto = true) + { + $address = trim($address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + // Don't validate now addresses with IDN. Will be done in send(). + $pos = strrpos($address, '@'); + if ((false === $pos) + || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported()) + && !static::validateAddress($address)) + ) { + $error_message = sprintf( + '%s (From): %s', + $this->lang('invalid_address'), + $address + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + $this->From = $address; + $this->FromName = $name; + if ($auto && empty($this->Sender)) { + $this->Sender = $address; + } + + return true; + } + + /** + * Return the Message-ID header of the last email. + * Technically this is the value from the last time the headers were created, + * but it's also the message ID of the last sent message except in + * pathological cases. + * + * @return string + */ + public function getLastMessageID() + { + return $this->lastMessageID; + } + + /** + * Check that a string looks like an email address. + * Validation patterns supported: + * * `auto` Pick best pattern automatically; + * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0; + * * `pcre` Use old PCRE implementation; + * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; + * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. + * * `noregex` Don't use a regex: super fast, really dumb. + * Alternatively you may pass in a callable to inject your own validator, for example: + * + * ```php + * PHPMailer::validateAddress('user@example.com', function($address) { + * return (strpos($address, '@') !== false); + * }); + * ``` + * + * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. + * + * @param string $address The email address to check + * @param string|callable $patternselect Which pattern to use + * + * @return bool + */ + public static function validateAddress($address, $patternselect = null) + { + if (null === $patternselect) { + $patternselect = static::$validator; + } + if (is_callable($patternselect)) { + return $patternselect($address); + } + //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 + if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) { + return false; + } + switch ($patternselect) { + case 'pcre': //Kept for BC + case 'pcre8': + /* + * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL + * is based. + * In addition to the addresses allowed by filter_var, also permits: + * * dotless domains: `a@b` + * * comments: `1234 @ local(blah) .machine .example` + * * quoted elements: `'"test blah"@example.org'` + * * numeric TLDs: `a@b.123` + * * unbracketed IPv4 literals: `a@192.168.0.1` + * * IPv6 literals: 'first.last@[IPv6:a1::]' + * Not all of these will necessarily work for sending! + * + * @see http://squiloople.com/2009/12/20/email-address-validation/ + * @copyright 2009-2010 Michael Rushton + * Feel free to use and redistribute this code. But please keep this copyright notice. + */ + return (bool) preg_match( + '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . + '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . + '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . + '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' . + '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' . + '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' . + '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' . + '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' . + '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', + $address + ); + case 'html5': + /* + * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. + * + * @see http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email) + */ + return (bool) preg_match( + '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . + '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', + $address + ); + case 'php': + default: + return (bool) filter_var($address, FILTER_VALIDATE_EMAIL); + } + } + + /** + * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the + * `intl` and `mbstring` PHP extensions. + * + * @return bool `true` if required functions for IDN support are present + */ + public static function idnSupported() + { + return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding'); + } + + /** + * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. + * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. + * This function silently returns unmodified address if: + * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) + * - Conversion to punycode is impossible (e.g. required PHP functions are not available) + * or fails for any reason (e.g. domain contains characters not allowed in an IDN). + * + * @see PHPMailer::$CharSet + * + * @param string $address The email address to convert + * + * @return string The encoded address in ASCII form + */ + public function punyencodeAddress($address) + { + // Verify we have required functions, CharSet, and at-sign. + $pos = strrpos($address, '@'); + if (!empty($this->CharSet) && + false !== $pos && + static::idnSupported() + ) { + $domain = substr($address, ++$pos); + // Verify CharSet string is a valid one, and domain properly encoded in this CharSet. + if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) { + $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet); + //Ignore IDE complaints about this line - method signature changed in PHP 5.4 + $errorcode = 0; + $punycode = idn_to_ascii($domain, $errorcode, INTL_IDNA_VARIANT_UTS46); + if (false !== $punycode) { + return substr($address, 0, $pos) . $punycode; + } + } + } + + return $address; + } + + /** + * Create a message and send it. + * Uses the sending method specified by $Mailer. + * + * @throws Exception + * + * @return bool false on error - See the ErrorInfo property for details of the error + */ + public function send() + { + try { + if (!$this->preSend()) { + return false; + } + + return $this->postSend(); + } catch (Exception $exc) { + $this->mailHeader = ''; + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + } + + /** + * Prepare a message for sending. + * + * @throws Exception + * + * @return bool + */ + public function preSend() + { + if ('smtp' === $this->Mailer + || ('mail' === $this->Mailer && stripos(PHP_OS, 'WIN') === 0) + ) { + //SMTP mandates RFC-compliant line endings + //and it's also used with mail() on Windows + static::setLE("\r\n"); + } else { + //Maintain backward compatibility with legacy Linux command line mailers + static::setLE(PHP_EOL); + } + //Check for buggy PHP versions that add a header with an incorrect line break + if ('mail' === $this->Mailer + && ((PHP_VERSION_ID >= 70000 && PHP_VERSION_ID < 70017) + || (PHP_VERSION_ID >= 70100 && PHP_VERSION_ID < 70103)) + && ini_get('mail.add_x_header') === '1' + && stripos(PHP_OS, 'WIN') === 0 + ) { + trigger_error( + 'Your version of PHP is affected by a bug that may result in corrupted messages.' . + ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' . + ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.', + E_USER_WARNING + ); + } + + try { + $this->error_count = 0; // Reset errors + $this->mailHeader = ''; + + // Dequeue recipient and Reply-To addresses with IDN + foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { + $params[1] = $this->punyencodeAddress($params[1]); + call_user_func_array([$this, 'addAnAddress'], $params); + } + if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { + throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); + } + + // Validate From, Sender, and ConfirmReadingTo addresses + foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) { + $this->$address_kind = trim($this->$address_kind); + if (empty($this->$address_kind)) { + continue; + } + $this->$address_kind = $this->punyencodeAddress($this->$address_kind); + if (!static::validateAddress($this->$address_kind)) { + $error_message = sprintf( + '%s (%s): %s', + $this->lang('invalid_address'), + $address_kind, + $this->$address_kind + ); + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new Exception($error_message); + } + + return false; + } + } + + // Set whether the message is multipart/alternative + if ($this->alternativeExists()) { + $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE; + } + + $this->setMessageType(); + // Refuse to send an empty message unless we are specifically allowing it + if (!$this->AllowEmpty && empty($this->Body)) { + throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + } + + //Trim subject consistently + $this->Subject = trim($this->Subject); + // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) + $this->MIMEHeader = ''; + $this->MIMEBody = $this->createBody(); + // createBody may have added some headers, so retain them + $tempheaders = $this->MIMEHeader; + $this->MIMEHeader = $this->createHeader(); + $this->MIMEHeader .= $tempheaders; + + // To capture the complete message when using mail(), create + // an extra header list which createHeader() doesn't fold in + if ('mail' === $this->Mailer) { + if (count($this->to) > 0) { + $this->mailHeader .= $this->addrAppend('To', $this->to); + } else { + $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;'); + } + $this->mailHeader .= $this->headerLine( + 'Subject', + $this->encodeHeader($this->secureHeader($this->Subject)) + ); + } + + // Sign with DKIM if enabled + if (!empty($this->DKIM_domain) + && !empty($this->DKIM_selector) + && (!empty($this->DKIM_private_string) + || (!empty($this->DKIM_private) + && static::isPermittedPath($this->DKIM_private) + && file_exists($this->DKIM_private) + ) + ) + ) { + $header_dkim = $this->DKIM_Add( + $this->MIMEHeader . $this->mailHeader, + $this->encodeHeader($this->secureHeader($this->Subject)), + $this->MIMEBody + ); + $this->MIMEHeader = rtrim($this->MIMEHeader, "\r\n ") . static::$LE . + static::normalizeBreaks($header_dkim) . static::$LE; + } + + return true; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + } + + /** + * Actually send a message via the selected mechanism. + * + * @throws Exception + * + * @return bool + */ + public function postSend() + { + try { + // Choose the mailer and send through it + switch ($this->Mailer) { + case 'sendmail': + case 'qmail': + return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody); + case 'smtp': + return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); + case 'mail': + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + default: + $sendMethod = $this->Mailer . 'Send'; + if (method_exists($this, $sendMethod)) { + return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody); + } + + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + } + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + } + + return false; + } + + /** + * Send mail using the $Sendmail program. + * + * @see PHPMailer::$Sendmail + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function sendmailSend($header, $body) + { + $header = rtrim($header, "\r\n ") . static::$LE . static::$LE; + + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (!empty($this->Sender) && self::isShellSafe($this->Sender)) { + if ('qmail' === $this->Mailer) { + $sendmailFmt = '%s -f%s'; + } else { + $sendmailFmt = '%s -oi -f%s -t'; + } + } elseif ('qmail' === $this->Mailer) { + $sendmailFmt = '%s'; + } else { + $sendmailFmt = '%s -oi -t'; + } + + $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); + + if ($this->SingleTo) { + foreach ($this->SingleToArray as $toAddr) { + $mail = @popen($sendmail, 'w'); + if (!$mail) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + fwrite($mail, 'To: ' . $toAddr . "\n"); + fwrite($mail, $header); + fwrite($mail, $body); + $result = pclose($mail); + $this->doCallback( + ($result === 0), + [$toAddr], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + if (0 !== $result) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + } else { + $mail = @popen($sendmail, 'w'); + if (!$mail) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + fwrite($mail, $header); + fwrite($mail, $body); + $result = pclose($mail); + $this->doCallback( + ($result === 0), + $this->to, + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + if (0 !== $result) { + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + + return true; + } + + /** + * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. + * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows. + * + * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report + * + * @param string $string The string to be validated + * + * @return bool + */ + protected static function isShellSafe($string) + { + // Future-proof + if (escapeshellcmd($string) !== $string + || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""]) + ) { + return false; + } + + $length = strlen($string); + + for ($i = 0; $i < $length; ++$i) { + $c = $string[$i]; + + // All other characters have a special meaning in at least one common shell, including = and +. + // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here. + // Note that this does permit non-Latin alphanumeric characters based on the current locale. + if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { + return false; + } + } + + return true; + } + + /** + * Check whether a file path is of a permitted type. + * Used to reject URLs and phar files from functions that access local file paths, + * such as addAttachment. + * + * @param string $path A relative or absolute path to a file + * + * @return bool + */ + protected static function isPermittedPath($path) + { + return !preg_match('#^[a-z]+://#i', $path); + } + + /** + * Send mail using the PHP mail() function. + * + * @see http://www.php.net/manual/en/book.mail.php + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function mailSend($header, $body) + { + $header = rtrim($header, "\r\n ") . static::$LE . static::$LE; + + $toArr = []; + foreach ($this->to as $toaddr) { + $toArr[] = $this->addrFormat($toaddr); + } + $to = implode(', ', $toArr); + + $params = null; + //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver + //A space after `-f` is optional, but there is a long history of its presence + //causing problems, so we don't use one + //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html + //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html + //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html + //Example problem: https://www.drupal.org/node/1057954 + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) { + $params = sprintf('-f%s', $this->Sender); + } + if (!empty($this->Sender) && static::validateAddress($this->Sender)) { + $old_from = ini_get('sendmail_from'); + ini_set('sendmail_from', $this->Sender); + } + $result = false; + if ($this->SingleTo && count($toArr) > 1) { + foreach ($toArr as $toAddr) { + $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); + $this->doCallback($result, [$toAddr], $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); + } + } else { + $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); + $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); + } + if (isset($old_from)) { + ini_set('sendmail_from', $old_from); + } + if (!$result) { + throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); + } + + return true; + } + + /** + * Get an instance to use for SMTP operations. + * Override this function to load your own SMTP implementation, + * or set one with setSMTPInstance. + * + * @return SMTP + */ + public function getSMTPInstance() + { + if (!is_object($this->smtp)) { + $this->smtp = new SMTP(); + } + + return $this->smtp; + } + + /** + * Provide an instance to use for SMTP operations. + * + * @return SMTP + */ + public function setSMTPInstance(SMTP $smtp) + { + $this->smtp = $smtp; + + return $this->smtp; + } + + /** + * Send mail via SMTP. + * Returns false if there is a bad MAIL FROM, RCPT, or DATA input. + * + * @see PHPMailer::setSMTPInstance() to use a different class. + * + * @uses \PHPMailer\PHPMailer\SMTP + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws Exception + * + * @return bool + */ + protected function smtpSend($header, $body) + { + $header = rtrim($header, "\r\n ") . static::$LE . static::$LE; + $bad_rcpt = []; + if (!$this->smtpConnect($this->SMTPOptions)) { + throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); + } + //Sender already validated in preSend() + if ('' === $this->Sender) { + $smtp_from = $this->From; + } else { + $smtp_from = $this->Sender; + } + if (!$this->smtp->mail($smtp_from)) { + $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); + throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); + } + + $callbacks = []; + // Attempt to send to all recipients + foreach ([$this->to, $this->cc, $this->bcc] as $togroup) { + foreach ($togroup as $to) { + if (!$this->smtp->recipient($to[0], $this->dsn)) { + $error = $this->smtp->getError(); + $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']]; + $isSent = false; + } else { + $isSent = true; + } + + $callbacks[] = ['issent'=>$isSent, 'to'=>$to[0]]; + } + } + + // Only send the DATA command if we have viable recipients + if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) { + throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); + } + + $smtp_transaction_id = $this->smtp->getLastTransactionID(); + + if ($this->SMTPKeepAlive) { + $this->smtp->reset(); + } else { + $this->smtp->quit(); + $this->smtp->close(); + } + + foreach ($callbacks as $cb) { + $this->doCallback( + $cb['issent'], + [$cb['to']], + [], + [], + $this->Subject, + $body, + $this->From, + ['smtp_transaction_id' => $smtp_transaction_id] + ); + } + + //Create error message for any bad addresses + if (count($bad_rcpt) > 0) { + $errstr = ''; + foreach ($bad_rcpt as $bad) { + $errstr .= $bad['to'] . ': ' . $bad['error']; + } + throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE); + } + + return true; + } + + /** + * Initiate a connection to an SMTP server. + * Returns false if the operation failed. + * + * @param array $options An array of options compatible with stream_context_create() + * + * @throws Exception + * + * @uses \PHPMailer\PHPMailer\SMTP + * + * @return bool + */ + public function smtpConnect($options = null) + { + if (null === $this->smtp) { + $this->smtp = $this->getSMTPInstance(); + } + + //If no options are provided, use whatever is set in the instance + if (null === $options) { + $options = $this->SMTPOptions; + } + + // Already connected? + if ($this->smtp->connected()) { + return true; + } + + $this->smtp->setTimeout($this->Timeout); + $this->smtp->setDebugLevel($this->SMTPDebug); + $this->smtp->setDebugOutput($this->Debugoutput); + $this->smtp->setVerp($this->do_verp); + $hosts = explode(';', $this->Host); + $lastexception = null; + + foreach ($hosts as $hostentry) { + $hostinfo = []; + if (!preg_match( + '/^((ssl|tls):\/\/)*([a-zA-Z\d.-]*|\[[a-fA-F\d:]+]):?(\d*)$/', + trim($hostentry), + $hostinfo + )) { + $this->edebug($this->lang('connect_host') . ' ' . $hostentry); + // Not a valid host entry + continue; + } + // $hostinfo[2]: optional ssl or tls prefix + // $hostinfo[3]: the hostname + // $hostinfo[4]: optional port number + // The host string prefix can temporarily override the current setting for SMTPSecure + // If it's not specified, the default value is used + + //Check the host name is a valid name or IP address before trying to use it + if (!static::isValidHost($hostinfo[3])) { + $this->edebug($this->lang('connect_host') . ' ' . $hostentry); + continue; + } + $prefix = ''; + $secure = $this->SMTPSecure; + $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure); + if ('ssl' === $hostinfo[2] || ('' === $hostinfo[2] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) { + $prefix = 'ssl://'; + $tls = false; // Can't have SSL and TLS at the same time + $secure = static::ENCRYPTION_SMTPS; + } elseif ('tls' === $hostinfo[2]) { + $tls = true; + // tls doesn't use a prefix + $secure = static::ENCRYPTION_STARTTLS; + } + //Do we need the OpenSSL extension? + $sslext = defined('OPENSSL_ALGO_SHA256'); + if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) { + //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled + if (!$sslext) { + throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); + } + } + $host = $hostinfo[3]; + $port = $this->Port; + $tport = (int) $hostinfo[4]; + if ($tport > 0 && $tport < 65536) { + $port = $tport; + } + if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) { + try { + if ($this->Helo) { + $hello = $this->Helo; + } else { + $hello = $this->serverHostname(); + } + $this->smtp->hello($hello); + //Automatically enable TLS encryption if: + // * it's not disabled + // * we have openssl extension + // * we are not already using SSL + // * the server offers STARTTLS + if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')) { + $tls = true; + } + if ($tls) { + if (!$this->smtp->startTLS()) { + throw new Exception($this->lang('connect_host')); + } + // We must resend EHLO after TLS negotiation + $this->smtp->hello($hello); + } + if ($this->SMTPAuth && !$this->smtp->authenticate( + $this->Username, + $this->Password, + $this->AuthType, + $this->oauth + )) { + throw new Exception($this->lang('authenticate')); + } + + return true; + } catch (Exception $exc) { + $lastexception = $exc; + $this->edebug($exc->getMessage()); + // We must have connected, but then failed TLS or Auth, so close connection nicely + $this->smtp->quit(); + } + } + } + // If we get here, all connection attempts have failed, so close connection hard + $this->smtp->close(); + // As we've caught all exceptions, just report whatever the last one was + if ($this->exceptions && null !== $lastexception) { + throw $lastexception; + } + + return false; + } + + /** + * Close the active SMTP session if one exists. + */ + public function smtpClose() + { + if ((null !== $this->smtp) && $this->smtp->connected()) { + $this->smtp->quit(); + $this->smtp->close(); + } + } + + /** + * Set the language for error messages. + * Returns false if it cannot load the language file. + * The default language is English. + * + * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") + * @param string $lang_path Path to the language file directory, with trailing separator (slash) + * + * @return bool + */ + public function setLanguage($langcode = 'en', $lang_path = '') + { + // Backwards compatibility for renamed language codes + $renamed_langcodes = [ + 'br' => 'pt_br', + 'cz' => 'cs', + 'dk' => 'da', + 'no' => 'nb', + 'se' => 'sv', + 'rs' => 'sr', + 'tg' => 'tl', + ]; + + if (isset($renamed_langcodes[$langcode])) { + $langcode = $renamed_langcodes[$langcode]; + } + + // Define full set of translatable strings in English + $PHPMAILER_LANG = [ + 'authenticate' => 'SMTP Error: Could not authenticate.', + 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', + 'data_not_accepted' => 'SMTP Error: data not accepted.', + 'empty_message' => 'Message body empty', + 'encoding' => 'Unknown encoding: ', + 'execute' => 'Could not execute: ', + 'file_access' => 'Could not access file: ', + 'file_open' => 'File Error: Could not open file: ', + 'from_failed' => 'The following From address failed: ', + 'instantiate' => 'Could not instantiate mail function.', + 'invalid_address' => 'Invalid address: ', + 'mailer_not_supported' => ' mailer is not supported.', + 'provide_address' => 'You must provide at least one recipient email address.', + 'recipients_failed' => 'SMTP Error: The following recipients failed: ', + 'signing' => 'Signing Error: ', + 'smtp_connect_failed' => 'SMTP connect() failed.', + 'smtp_error' => 'SMTP server error: ', + 'variable_set' => 'Cannot set or reset variable: ', + 'extension_missing' => 'Extension missing: ', + ]; + if (empty($lang_path)) { + // Calculate an absolute path so it can work if CWD is not here + $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; + } + //Validate $langcode + if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { + $langcode = 'en'; + } + $foundlang = true; + $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; + // There is no English translation file + if ('en' !== $langcode) { + // Make sure language file path is readable + if (!static::isPermittedPath($lang_file) || !file_exists($lang_file)) { + $foundlang = false; + } else { + // Overwrite language-specific strings. + // This way we'll never have missing translation keys. + $foundlang = include $lang_file; + } + } + $this->language = $PHPMAILER_LANG; + + return (bool) $foundlang; // Returns false if language not found + } + + /** + * Get the array of strings for the current language. + * + * @return array + */ + public function getTranslations() + { + return $this->language; + } + + /** + * Create recipient headers. + * + * @param string $type + * @param array $addr An array of recipients, + * where each recipient is a 2-element indexed array with element 0 containing an address + * and element 1 containing a name, like: + * [['joe@example.com', 'Joe User'], ['zoe@example.com', 'Zoe User']] + * + * @return string + */ + public function addrAppend($type, $addr) + { + $addresses = []; + foreach ($addr as $address) { + $addresses[] = $this->addrFormat($address); + } + + return $type . ': ' . implode(', ', $addresses) . static::$LE; + } + + /** + * Format an address for use in a message header. + * + * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like + * ['joe@example.com', 'Joe User'] + * + * @return string + */ + public function addrFormat($addr) + { + if (empty($addr[1])) { // No name provided + return $this->secureHeader($addr[0]); + } + + return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . + ' <' . $this->secureHeader($addr[0]) . '>'; + } + + /** + * Word-wrap message. + * For use with mailers that do not automatically perform wrapping + * and for quoted-printable encoded messages. + * Original written by philippe. + * + * @param string $message The message to wrap + * @param int $length The line length to wrap to + * @param bool $qp_mode Whether to run in Quoted-Printable mode + * + * @return string + */ + public function wrapText($message, $length, $qp_mode = false) + { + if ($qp_mode) { + $soft_break = sprintf(' =%s', static::$LE); + } else { + $soft_break = static::$LE; + } + // If utf-8 encoding is used, we will need to make sure we don't + // split multibyte characters when we wrap + $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet); + $lelen = strlen(static::$LE); + $crlflen = strlen(static::$LE); + + $message = static::normalizeBreaks($message); + //Remove a trailing line break + if (substr($message, -$lelen) === static::$LE) { + $message = substr($message, 0, -$lelen); + } + + //Split message into lines + $lines = explode(static::$LE, $message); + //Message will be rebuilt in here + $message = ''; + foreach ($lines as $line) { + $words = explode(' ', $line); + $buf = ''; + $firstword = true; + foreach ($words as $word) { + if ($qp_mode && (strlen($word) > $length)) { + $space_left = $length - strlen($buf) - $crlflen; + if (!$firstword) { + if ($space_left > 20) { + $len = $space_left; + if ($is_utf8) { + $len = $this->utf8CharBoundary($word, $len); + } elseif ('=' === substr($word, $len - 1, 1)) { + --$len; + } elseif ('=' === substr($word, $len - 2, 1)) { + $len -= 2; + } + $part = substr($word, 0, $len); + $word = substr($word, $len); + $buf .= ' ' . $part; + $message .= $buf . sprintf('=%s', static::$LE); + } else { + $message .= $buf . $soft_break; + } + $buf = ''; + } + while ($word !== '') { + if ($length <= 0) { + break; + } + $len = $length; + if ($is_utf8) { + $len = $this->utf8CharBoundary($word, $len); + } elseif ('=' === substr($word, $len - 1, 1)) { + --$len; + } elseif ('=' === substr($word, $len - 2, 1)) { + $len -= 2; + } + $part = substr($word, 0, $len); + $word = (string) substr($word, $len); + + if ($word !== '') { + $message .= $part . sprintf('=%s', static::$LE); + } else { + $buf = $part; + } + } + } else { + $buf_o = $buf; + if (!$firstword) { + $buf .= ' '; + } + $buf .= $word; + + if ('' !== $buf_o && strlen($buf) > $length) { + $message .= $buf_o . $soft_break; + $buf = $word; + } + } + $firstword = false; + } + $message .= $buf . static::$LE; + } + + return $message; + } + + /** + * Find the last character boundary prior to $maxLength in a utf-8 + * quoted-printable encoded string. + * Original written by Colin Brown. + * + * @param string $encodedText utf-8 QP text + * @param int $maxLength Find the last character boundary prior to this length + * + * @return int + */ + public function utf8CharBoundary($encodedText, $maxLength) + { + $foundSplitPos = false; + $lookBack = 3; + while (!$foundSplitPos) { + $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack); + $encodedCharPos = strpos($lastChunk, '='); + if (false !== $encodedCharPos) { + // Found start of encoded character byte within $lookBack block. + // Check the encoded byte value (the 2 chars after the '=') + $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2); + $dec = hexdec($hex); + if ($dec < 128) { + // Single byte character. + // If the encoded char was found at pos 0, it will fit + // otherwise reduce maxLength to start of the encoded char + if ($encodedCharPos > 0) { + $maxLength -= $lookBack - $encodedCharPos; + } + $foundSplitPos = true; + } elseif ($dec >= 192) { + // First byte of a multi byte character + // Reduce maxLength to split at start of character + $maxLength -= $lookBack - $encodedCharPos; + $foundSplitPos = true; + } elseif ($dec < 192) { + // Middle byte of a multi byte character, look further back + $lookBack += 3; + } + } else { + // No encoded character found + $foundSplitPos = true; + } + } + + return $maxLength; + } + + /** + * Apply word wrapping to the message body. + * Wraps the message body to the number of chars set in the WordWrap property. + * You should only do this to plain-text bodies as wrapping HTML tags may break them. + * This is called automatically by createBody(), so you don't need to call it yourself. + */ + public function setWordWrap() + { + if ($this->WordWrap < 1) { + return; + } + + switch ($this->message_type) { + case 'alt': + case 'alt_inline': + case 'alt_attach': + case 'alt_inline_attach': + $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap); + break; + default: + $this->Body = $this->wrapText($this->Body, $this->WordWrap); + break; + } + } + + /** + * Assemble message headers. + * + * @return string The assembled headers + */ + public function createHeader() + { + $result = ''; + + $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate); + + // To be created automatically by mail() + if ($this->SingleTo) { + if ('mail' !== $this->Mailer) { + foreach ($this->to as $toaddr) { + $this->SingleToArray[] = $this->addrFormat($toaddr); + } + } + } elseif (count($this->to) > 0) { + if ('mail' !== $this->Mailer) { + $result .= $this->addrAppend('To', $this->to); + } + } elseif (count($this->cc) === 0) { + $result .= $this->headerLine('To', 'undisclosed-recipients:;'); + } + + $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]); + + // sendmail and mail() extract Cc from the header before sending + if (count($this->cc) > 0) { + $result .= $this->addrAppend('Cc', $this->cc); + } + + // sendmail and mail() extract Bcc from the header before sending + if (( + 'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer + ) + && count($this->bcc) > 0 + ) { + $result .= $this->addrAppend('Bcc', $this->bcc); + } + + if (count($this->ReplyTo) > 0) { + $result .= $this->addrAppend('Reply-To', $this->ReplyTo); + } + + // mail() sets the subject itself + if ('mail' !== $this->Mailer) { + $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject))); + } + + // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4 + // https://tools.ietf.org/html/rfc5322#section-3.6.4 + if ('' !== $this->MessageID && preg_match('/^<.*@.*>$/', $this->MessageID)) { + $this->lastMessageID = $this->MessageID; + } else { + $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname()); + } + $result .= $this->headerLine('Message-ID', $this->lastMessageID); + if (null !== $this->Priority) { + $result .= $this->headerLine('X-Priority', $this->Priority); + } + if ('' === $this->XMailer) { + $result .= $this->headerLine( + 'X-Mailer', + 'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)' + ); + } else { + $myXmailer = trim($this->XMailer); + if ($myXmailer) { + $result .= $this->headerLine('X-Mailer', $myXmailer); + } + } + + if ('' !== $this->ConfirmReadingTo) { + $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>'); + } + + // Add custom headers + foreach ($this->CustomHeader as $header) { + $result .= $this->headerLine( + trim($header[0]), + $this->encodeHeader(trim($header[1])) + ); + } + if (!$this->sign_key_file) { + $result .= $this->headerLine('MIME-Version', '1.0'); + $result .= $this->getMailMIME(); + } + + return $result; + } + + /** + * Get the message MIME type headers. + * + * @return string + */ + public function getMailMIME() + { + $result = ''; + $ismultipart = true; + switch ($this->message_type) { + case 'inline': + $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"'); + break; + case 'attach': + case 'inline_attach': + case 'alt_attach': + case 'alt_inline_attach': + $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';'); + $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"'); + break; + case 'alt': + case 'alt_inline': + $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); + $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"'); + break; + default: + // Catches case 'plain': and case '': + $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet); + $ismultipart = false; + break; + } + // RFC1341 part 5 says 7bit is assumed if not specified + if (static::ENCODING_7BIT !== $this->Encoding) { + // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE + if ($ismultipart) { + if (static::ENCODING_8BIT === $this->Encoding) { + $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT); + } + // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible + } else { + $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding); + } + } + + if ('mail' !== $this->Mailer) { +// $result .= static::$LE; + } + + return $result; + } + + /** + * Returns the whole MIME message. + * Includes complete headers and body. + * Only valid post preSend(). + * + * @see PHPMailer::preSend() + * + * @return string + */ + public function getSentMIMEMessage() + { + return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . static::$LE . static::$LE . $this->MIMEBody; + } + + /** + * Create a unique ID to use for boundaries. + * + * @return string + */ + protected function generateId() + { + $len = 32; //32 bytes = 256 bits + $bytes = ''; + if (function_exists('random_bytes')) { + try { + $bytes = random_bytes($len); + } catch (\Exception $e) { + //Do nothing + } + } elseif (function_exists('openssl_random_pseudo_bytes')) { + /** @noinspection CryptographicallySecureRandomnessInspection */ + $bytes = openssl_random_pseudo_bytes($len); + } + if ($bytes === '') { + //We failed to produce a proper random string, so make do. + //Use a hash to force the length to the same as the other methods + $bytes = hash('sha256', uniqid((string) mt_rand(), true), true); + } + + //We don't care about messing up base64 format here, just want a random string + return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true))); + } + + /** + * Assemble the message body. + * Returns an empty string on failure. + * + * @throws Exception + * + * @return string The assembled message body + */ + public function createBody() + { + $body = ''; + //Create unique IDs and preset boundaries + $this->uniqueid = $this->generateId(); + $this->boundary[1] = 'b1_' . $this->uniqueid; + $this->boundary[2] = 'b2_' . $this->uniqueid; + $this->boundary[3] = 'b3_' . $this->uniqueid; + + if ($this->sign_key_file) { + $body .= $this->getMailMIME() . static::$LE; + } + + $this->setWordWrap(); + + $bodyEncoding = $this->Encoding; + $bodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) { + $bodyEncoding = static::ENCODING_7BIT; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $bodyCharSet = static::CHARSET_ASCII; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the body part only + if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) { + $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE; + } + + $altBodyEncoding = $this->Encoding; + $altBodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) { + $altBodyEncoding = static::ENCODING_7BIT; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $altBodyCharSet = static::CHARSET_ASCII; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the alt body part only + if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) { + $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE; + } + //Use this as a preamble in all multipart message types + $mimepre = 'This is a multi-part message in MIME format.' . static::$LE; + switch ($this->message_type) { + case 'inline': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[1]); + break; + case 'attach': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'inline_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";'); + $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"'); + $body .= static::$LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[2]); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'alt': + $body .= $mimepre; + $body .= $this->getBoundary( + $this->boundary[1], + $altBodyCharSet, + static::CONTENT_TYPE_PLAINTEXT, + $altBodyEncoding + ); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->getBoundary( + $this->boundary[1], + $bodyCharSet, + static::CONTENT_TYPE_TEXT_HTML, + $bodyEncoding + ); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + if (!empty($this->Ical)) { + $method = static::ICAL_METHOD_REQUEST; + foreach (static::$IcalMethods as $imethod) { + if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) { + $method = $imethod; + break; + } + } + $body .= $this->getBoundary( + $this->boundary[1], + '', + static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method, + '' + ); + $body .= $this->encodeString($this->Ical, $this->Encoding); + $body .= static::$LE; + } + $body .= $this->endBoundary($this->boundary[1]); + break; + case 'alt_inline': + $body .= $mimepre; + $body .= $this->getBoundary( + $this->boundary[1], + $altBodyCharSet, + static::CONTENT_TYPE_PLAINTEXT, + $altBodyEncoding + ); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";'); + $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"'); + $body .= static::$LE; + $body .= $this->getBoundary( + $this->boundary[2], + $bodyCharSet, + static::CONTENT_TYPE_TEXT_HTML, + $bodyEncoding + ); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[2]); + $body .= static::$LE; + $body .= $this->endBoundary($this->boundary[1]); + break; + case 'alt_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); + $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary( + $this->boundary[2], + $altBodyCharSet, + static::CONTENT_TYPE_PLAINTEXT, + $altBodyEncoding + ); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->getBoundary( + $this->boundary[2], + $bodyCharSet, + static::CONTENT_TYPE_TEXT_HTML, + $bodyEncoding + ); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + if (!empty($this->Ical)) { + $method = static::ICAL_METHOD_REQUEST; + foreach (static::$IcalMethods as $imethod) { + if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) { + $method = $imethod; + break; + } + } + $body .= $this->getBoundary( + $this->boundary[2], + '', + static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method, + '' + ); + $body .= $this->encodeString($this->Ical, $this->Encoding); + } + $body .= $this->endBoundary($this->boundary[2]); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'alt_inline_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';'); + $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"'); + $body .= static::$LE; + $body .= $this->getBoundary( + $this->boundary[2], + $altBodyCharSet, + static::CONTENT_TYPE_PLAINTEXT, + $altBodyEncoding + ); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= static::$LE; + $body .= $this->textLine('--' . $this->boundary[2]); + $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';'); + $body .= $this->textLine(' boundary="' . $this->boundary[3] . '";'); + $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"'); + $body .= static::$LE; + $body .= $this->getBoundary( + $this->boundary[3], + $bodyCharSet, + static::CONTENT_TYPE_TEXT_HTML, + $bodyEncoding + ); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= static::$LE; + $body .= $this->attachAll('inline', $this->boundary[3]); + $body .= static::$LE; + $body .= $this->endBoundary($this->boundary[2]); + $body .= static::$LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + default: + // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types + //Reset the `Encoding` property in case we changed it for line length reasons + $this->Encoding = $bodyEncoding; + $body .= $this->encodeString($this->Body, $this->Encoding); + break; + } + + if ($this->isError()) { + $body = ''; + if ($this->exceptions) { + throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + } + } elseif ($this->sign_key_file) { + try { + if (!defined('PKCS7_TEXT')) { + throw new Exception($this->lang('extension_missing') . 'openssl'); + } + + $file = tempnam(sys_get_temp_dir(), 'srcsign'); + $signed = tempnam(sys_get_temp_dir(), 'mailsign'); + file_put_contents($file, $body); + + //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 + if (empty($this->sign_extracerts_file)) { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + ['file://' . realpath($this->sign_key_file), $this->sign_key_pass], + [] + ); + } else { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + ['file://' . realpath($this->sign_key_file), $this->sign_key_pass], + [], + PKCS7_DETACHED, + $this->sign_extracerts_file + ); + } + + @unlink($file); + if ($sign) { + $body = file_get_contents($signed); + @unlink($signed); + //The message returned by openssl contains both headers and body, so need to split them up + $parts = explode("\n\n", $body, 2); + $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE; + $body = $parts[1]; + } else { + @unlink($signed); + throw new Exception($this->lang('signing') . openssl_error_string()); + } + } catch (Exception $exc) { + $body = ''; + if ($this->exceptions) { + throw $exc; + } + } + } + + return $body; + } + + /** + * Return the start of a message boundary. + * + * @param string $boundary + * @param string $charSet + * @param string $contentType + * @param string $encoding + * + * @return string + */ + protected function getBoundary($boundary, $charSet, $contentType, $encoding) + { + $result = ''; + if ('' === $charSet) { + $charSet = $this->CharSet; + } + if ('' === $contentType) { + $contentType = $this->ContentType; + } + if ('' === $encoding) { + $encoding = $this->Encoding; + } + $result .= $this->textLine('--' . $boundary); + $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet); + $result .= static::$LE; + // RFC1341 part 5 says 7bit is assumed if not specified + if (static::ENCODING_7BIT !== $encoding) { + $result .= $this->headerLine('Content-Transfer-Encoding', $encoding); + } + $result .= static::$LE; + + return $result; + } + + /** + * Return the end of a message boundary. + * + * @param string $boundary + * + * @return string + */ + protected function endBoundary($boundary) + { + return static::$LE . '--' . $boundary . '--' . static::$LE; + } + + /** + * Set the message type. + * PHPMailer only supports some preset message types, not arbitrary MIME structures. + */ + protected function setMessageType() + { + $type = []; + if ($this->alternativeExists()) { + $type[] = 'alt'; + } + if ($this->inlineImageExists()) { + $type[] = 'inline'; + } + if ($this->attachmentExists()) { + $type[] = 'attach'; + } + $this->message_type = implode('_', $type); + if ('' === $this->message_type) { + //The 'plain' message_type refers to the message having a single body element, not that it is plain-text + $this->message_type = 'plain'; + } + } + + /** + * Format a header line. + * + * @param string $name + * @param string|int $value + * + * @return string + */ + public function headerLine($name, $value) + { + return $name . ': ' . $value . static::$LE; + } + + /** + * Return a formatted mail line. + * + * @param string $value + * + * @return string + */ + public function textLine($value) + { + return $value . static::$LE; + } + + /** + * Add an attachment from a path on the filesystem. + * Never use a user-supplied path to a file! + * Returns false if the file could not be found or read. + * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client. + * If you need to do that, fetch the resource yourself and pass it in via a local file or string. + * + * @param string $path Path to the attachment + * @param string $name Overrides the attachment name + * @param string $encoding File encoding (see $Encoding) + * @param string $type File extension (MIME) type + * @param string $disposition Disposition to use + * + * @throws Exception + * + * @return bool + */ + public function addAttachment( + $path, + $name = '', + $encoding = self::ENCODING_BASE64, + $type = '', + $disposition = 'attachment' + ) { + try { + if (!static::isPermittedPath($path) || !@is_file($path)) { + throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); + } + + // If a MIME type is not specified, try to work it out from the file name + if ('' === $type) { + $type = static::filenameToType($path); + } + + $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); + if ('' === $name) { + $name = $filename; + } + + if (!$this->validateEncoding($encoding)) { + throw new Exception($this->lang('encoding') . $encoding); + } + + $this->attachment[] = [ + 0 => $path, + 1 => $filename, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => false, // isStringAttachment + 6 => $disposition, + 7 => $name, + ]; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + + return false; + } + + return true; + } + + /** + * Return the array of attachments. + * + * @return array + */ + public function getAttachments() + { + return $this->attachment; + } + + /** + * Attach all file, string, and binary attachments to the message. + * Returns an empty string on failure. + * + * @param string $disposition_type + * @param string $boundary + * + * @throws Exception + * + * @return string + */ + protected function attachAll($disposition_type, $boundary) + { + // Return text of body + $mime = []; + $cidUniq = []; + $incl = []; + + // Add all attachments + foreach ($this->attachment as $attachment) { + // Check if it is a valid disposition_filter + if ($attachment[6] === $disposition_type) { + // Check for string attachment + $string = ''; + $path = ''; + $bString = $attachment[5]; + if ($bString) { + $string = $attachment[0]; + } else { + $path = $attachment[0]; + } + + $inclhash = hash('sha256', serialize($attachment)); + if (in_array($inclhash, $incl, true)) { + continue; + } + $incl[] = $inclhash; + $name = $attachment[2]; + $encoding = $attachment[3]; + $type = $attachment[4]; + $disposition = $attachment[6]; + $cid = $attachment[7]; + if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) { + continue; + } + $cidUniq[$cid] = true; + + $mime[] = sprintf('--%s%s', $boundary, static::$LE); + //Only include a filename property if we have one + if (!empty($name)) { + $mime[] = sprintf( + 'Content-Type: %s; name="%s"%s', + $type, + $this->encodeHeader($this->secureHeader($name)), + static::$LE + ); + } else { + $mime[] = sprintf( + 'Content-Type: %s%s', + $type, + static::$LE + ); + } + // RFC1341 part 5 says 7bit is assumed if not specified + if (static::ENCODING_7BIT !== $encoding) { + $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE); + } + + //Only set Content-IDs on inline attachments + if ((string) $cid !== '' && $disposition === 'inline') { + $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE; + } + + // If a filename contains any of these chars, it should be quoted, + // but not otherwise: RFC2183 & RFC2045 5.1 + // Fixes a warning in IETF's msglint MIME checker + // Allow for bypassing the Content-Disposition header totally + if (!empty($disposition)) { + $encoded_name = $this->encodeHeader($this->secureHeader($name)); + if (preg_match('/[ ()<>@,;:"\/\[\]?=]/', $encoded_name)) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename="%s"%s', + $disposition, + $encoded_name, + static::$LE . static::$LE + ); + } elseif (!empty($encoded_name)) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename=%s%s', + $disposition, + $encoded_name, + static::$LE . static::$LE + ); + } else { + $mime[] = sprintf( + 'Content-Disposition: %s%s', + $disposition, + static::$LE . static::$LE + ); + } + } else { + $mime[] = static::$LE; + } + + // Encode as string attachment + if ($bString) { + $mime[] = $this->encodeString($string, $encoding); + } else { + $mime[] = $this->encodeFile($path, $encoding); + } + if ($this->isError()) { + return ''; + } + $mime[] = static::$LE; + } + } + + $mime[] = sprintf('--%s--%s', $boundary, static::$LE); + + return implode('', $mime); + } + + /** + * Encode a file attachment in requested format. + * Returns an empty string on failure. + * + * @param string $path The full path to the file + * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' + * + * @return string + */ + protected function encodeFile($path, $encoding = self::ENCODING_BASE64) + { + try { + if (!static::isPermittedPath($path) || !file_exists($path)) { + throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + } + $file_buffer = file_get_contents($path); + if (false === $file_buffer) { + throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + } + $file_buffer = $this->encodeString($file_buffer, $encoding); + + return $file_buffer; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + + return ''; + } + } + + /** + * Encode a string in requested format. + * Returns an empty string on failure. + * + * @param string $str The text to encode + * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' + * + * @throws Exception + * + * @return string + */ + public function encodeString($str, $encoding = self::ENCODING_BASE64) + { + $encoded = ''; + switch (strtolower($encoding)) { + case static::ENCODING_BASE64: + $encoded = chunk_split( + base64_encode($str), + static::STD_LINE_LENGTH, + static::$LE + ); + break; + case static::ENCODING_7BIT: + case static::ENCODING_8BIT: + $encoded = static::normalizeBreaks($str); + // Make sure it ends with a line break + if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) { + $encoded .= static::$LE; + } + break; + case static::ENCODING_BINARY: + $encoded = $str; + break; + case static::ENCODING_QUOTED_PRINTABLE: + $encoded = $this->encodeQP($str); + break; + default: + $this->setError($this->lang('encoding') . $encoding); + if ($this->exceptions) { + throw new Exception($this->lang('encoding') . $encoding); + } + break; + } + + return $encoded; + } + + /** + * Encode a header value (not including its label) optimally. + * Picks shortest of Q, B, or none. Result includes folding if needed. + * See RFC822 definitions for phrase, comment and text positions. + * + * @param string $str The header value to encode + * @param string $position What context the string will be used in + * + * @return string + */ + public function encodeHeader($str, $position = 'text') + { + $matchcount = 0; + switch (strtolower($position)) { + case 'phrase': + if (!preg_match('/[\200-\377]/', $str)) { + // Can't use addslashes as we don't know the value of magic_quotes_sybase + $encoded = addcslashes($str, "\0..\37\177\\\""); + if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) { + return $encoded; + } + + return "\"$encoded\""; + } + $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); + break; + /* @noinspection PhpMissingBreakStatementInspection */ + case 'comment': + $matchcount = preg_match_all('/[()"]/', $str, $matches); + //fallthrough + case 'text': + default: + $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); + break; + } + + if ($this->has8bitChars($str)) { + $charset = $this->CharSet; + } else { + $charset = static::CHARSET_ASCII; + } + + // Q/B encoding adds 8 chars and the charset ("` =? and must not be empty
+ * will look for an image file in $basedir/images/a.png and convert it to inline.
+ * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
+ * Converts data-uri images into embedded attachments.
+ * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
+ *
+ * @param string $message HTML message string
+ * @param string $basedir Absolute path to a base directory to prepend to relative paths to images
+ * @param bool|callable $advanced Whether to use the internal HTML to text converter
+ * or your own custom converter @return string $message The transformed message Body
+ *
+ * @throws Exception
+ *
+ * @see PHPMailer::html2text()
+ */
+ public function msgHTML($message, $basedir = '', $advanced = false)
+ {
+ preg_match_all('/(? 1 && '/' !== substr($basedir, -1)) {
+ // Ensure $basedir has a trailing /
+ $basedir .= '/';
+ }
+ foreach ($images[2] as $imgindex => $url) {
+ // Convert data URIs into embedded images
+ //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+ if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
+ if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {
+ $data = base64_decode($match[3]);
+ } elseif ('' === $match[2]) {
+ $data = rawurldecode($match[3]);
+ } else {
+ //Not recognised so leave it alone
+ continue;
+ }
+ //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places
+ //will only be embedded once, even if it used a different encoding
+ $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; // RFC2392 S 2
+
+ if (!$this->cidExists($cid)) {
+ $this->addStringEmbeddedImage(
+ $data,
+ $cid,
+ 'embed' . $imgindex,
+ static::ENCODING_BASE64,
+ $match[1]
+ );
+ }
+ $message = str_replace(
+ $images[0][$imgindex],
+ $images[1][$imgindex] . '="cid:' . $cid . '"',
+ $message
+ );
+ continue;
+ }
+ if (// Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
+ !empty($basedir)
+ // Ignore URLs containing parent dir traversal (..)
+ && (strpos($url, '..') === false)
+ // Do not change urls that are already inline images
+ && 0 !== strpos($url, 'cid:')
+ // Do not change absolute URLs, including anonymous protocol
+ && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
+ ) {
+ $filename = static::mb_pathinfo($url, PATHINFO_BASENAME);
+ $directory = dirname($url);
+ if ('.' === $directory) {
+ $directory = '';
+ }
+ // RFC2392 S 2
+ $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0';
+ if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
+ $basedir .= '/';
+ }
+ if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {
+ $directory .= '/';
+ }
+ if ($this->addEmbeddedImage(
+ $basedir . $directory . $filename,
+ $cid,
+ $filename,
+ static::ENCODING_BASE64,
+ static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
+ )
+ ) {
+ $message = preg_replace(
+ '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
+ $images[1][$imgindex] . '="cid:' . $cid . '"',
+ $message
+ );
+ }
+ }
+ }
+ }
+ $this->isHTML();
+ // Convert all message body line breaks to LE, makes quoted-printable encoding work much better
+ $this->Body = static::normalizeBreaks($message);
+ $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
+ if (!$this->alternativeExists()) {
+ $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
+ . static::$LE;
+ }
+
+ return $this->Body;
+ }
+
+ /**
+ * Convert an HTML string into plain text.
+ * This is used by msgHTML().
+ * Note - older versions of this function used a bundled advanced converter
+ * which was removed for license reasons in #232.
+ * Example usage:
+ *
+ * ```php
+ * // Use default conversion
+ * $plain = $mail->html2text($html);
+ * // Use your own custom converter
+ * $plain = $mail->html2text($html, function($html) {
+ * $converter = new MyHtml2text($html);
+ * return $converter->get_text();
+ * });
+ * ```
+ *
+ * @param string $html The HTML text to convert
+ * @param bool|callable $advanced Any boolean value to use the internal converter,
+ * or provide your own callable for custom conversion
+ *
+ * @return string
+ */
+ public function html2text($html, $advanced = false)
+ {
+ if (is_callable($advanced)) {
+ return $advanced($html);
+ }
+
+ return html_entity_decode(
+ trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
+ ENT_QUOTES,
+ $this->CharSet
+ );
+ }
+
+ /**
+ * Get the MIME type for a file extension.
+ *
+ * @param string $ext File extension
+ *
+ * @return string MIME type of file
+ */
+ public static function _mime_types($ext = '')
+ {
+ $mimes = [
+ 'xl' => 'application/excel',
+ 'js' => 'application/javascript',
+ 'hqx' => 'application/mac-binhex40',
+ 'cpt' => 'application/mac-compactpro',
+ 'bin' => 'application/macbinary',
+ 'doc' => 'application/msword',
+ 'word' => 'application/msword',
+ 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+ 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
+ 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+ 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
+ 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+ 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
+ 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
+ 'class' => 'application/octet-stream',
+ 'dll' => 'application/octet-stream',
+ 'dms' => 'application/octet-stream',
+ 'exe' => 'application/octet-stream',
+ 'lha' => 'application/octet-stream',
+ 'lzh' => 'application/octet-stream',
+ 'psd' => 'application/octet-stream',
+ 'sea' => 'application/octet-stream',
+ 'so' => 'application/octet-stream',
+ 'oda' => 'application/oda',
+ 'pdf' => 'application/pdf',
+ 'ai' => 'application/postscript',
+ 'eps' => 'application/postscript',
+ 'ps' => 'application/postscript',
+ 'smi' => 'application/smil',
+ 'smil' => 'application/smil',
+ 'mif' => 'application/vnd.mif',
+ 'xls' => 'application/vnd.ms-excel',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+ 'wbxml' => 'application/vnd.wap.wbxml',
+ 'wmlc' => 'application/vnd.wap.wmlc',
+ 'dcr' => 'application/x-director',
+ 'dir' => 'application/x-director',
+ 'dxr' => 'application/x-director',
+ 'dvi' => 'application/x-dvi',
+ 'gtar' => 'application/x-gtar',
+ 'php3' => 'application/x-httpd-php',
+ 'php4' => 'application/x-httpd-php',
+ 'php' => 'application/x-httpd-php',
+ 'phtml' => 'application/x-httpd-php',
+ 'phps' => 'application/x-httpd-php-source',
+ 'swf' => 'application/x-shockwave-flash',
+ 'sit' => 'application/x-stuffit',
+ 'tar' => 'application/x-tar',
+ 'tgz' => 'application/x-tar',
+ 'xht' => 'application/xhtml+xml',
+ 'xhtml' => 'application/xhtml+xml',
+ 'zip' => 'application/zip',
+ 'mid' => 'audio/midi',
+ 'midi' => 'audio/midi',
+ 'mp2' => 'audio/mpeg',
+ 'mp3' => 'audio/mpeg',
+ 'm4a' => 'audio/mp4',
+ 'mpga' => 'audio/mpeg',
+ 'aif' => 'audio/x-aiff',
+ 'aifc' => 'audio/x-aiff',
+ 'aiff' => 'audio/x-aiff',
+ 'ram' => 'audio/x-pn-realaudio',
+ 'rm' => 'audio/x-pn-realaudio',
+ 'rpm' => 'audio/x-pn-realaudio-plugin',
+ 'ra' => 'audio/x-realaudio',
+ 'wav' => 'audio/x-wav',
+ 'mka' => 'audio/x-matroska',
+ 'bmp' => 'image/bmp',
+ 'gif' => 'image/gif',
+ 'jpeg' => 'image/jpeg',
+ 'jpe' => 'image/jpeg',
+ 'jpg' => 'image/jpeg',
+ 'png' => 'image/png',
+ 'tiff' => 'image/tiff',
+ 'tif' => 'image/tiff',
+ 'webp' => 'image/webp',
+ 'heif' => 'image/heif',
+ 'heifs' => 'image/heif-sequence',
+ 'heic' => 'image/heic',
+ 'heics' => 'image/heic-sequence',
+ 'eml' => 'message/rfc822',
+ 'css' => 'text/css',
+ 'html' => 'text/html',
+ 'htm' => 'text/html',
+ 'shtml' => 'text/html',
+ 'log' => 'text/plain',
+ 'text' => 'text/plain',
+ 'txt' => 'text/plain',
+ 'rtx' => 'text/richtext',
+ 'rtf' => 'text/rtf',
+ 'vcf' => 'text/vcard',
+ 'vcard' => 'text/vcard',
+ 'ics' => 'text/calendar',
+ 'xml' => 'text/xml',
+ 'xsl' => 'text/xml',
+ 'wmv' => 'video/x-ms-wmv',
+ 'mpeg' => 'video/mpeg',
+ 'mpe' => 'video/mpeg',
+ 'mpg' => 'video/mpeg',
+ 'mp4' => 'video/mp4',
+ 'm4v' => 'video/mp4',
+ 'mov' => 'video/quicktime',
+ 'qt' => 'video/quicktime',
+ 'rv' => 'video/vnd.rn-realvideo',
+ 'avi' => 'video/x-msvideo',
+ 'movie' => 'video/x-sgi-movie',
+ 'webm' => 'video/webm',
+ 'mkv' => 'video/x-matroska',
+ ];
+ $ext = strtolower($ext);
+ if (array_key_exists($ext, $mimes)) {
+ return $mimes[$ext];
+ }
+
+ return 'application/octet-stream';
+ }
+
+ /**
+ * Map a file name to a MIME type.
+ * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
+ *
+ * @param string $filename A file name or full path, does not need to exist as a file
+ *
+ * @return string
+ */
+ public static function filenameToType($filename)
+ {
+ // In case the path is a URL, strip any query string before getting extension
+ $qpos = strpos($filename, '?');
+ if (false !== $qpos) {
+ $filename = substr($filename, 0, $qpos);
+ }
+ $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
+
+ return static::_mime_types($ext);
+ }
+
+ /**
+ * Multi-byte-safe pathinfo replacement.
+ * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
+ *
+ * @see http://www.php.net/manual/en/function.pathinfo.php#107461
+ *
+ * @param string $path A filename or path, does not need to exist as a file
+ * @param int|string $options Either a PATHINFO_* constant,
+ * or a string name to return only the specified piece
+ *
+ * @return string|array
+ */
+ public static function mb_pathinfo($path, $options = null)
+ {
+ $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
+ $pathinfo = [];
+ if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
+ if (array_key_exists(1, $pathinfo)) {
+ $ret['dirname'] = $pathinfo[1];
+ }
+ if (array_key_exists(2, $pathinfo)) {
+ $ret['basename'] = $pathinfo[2];
+ }
+ if (array_key_exists(5, $pathinfo)) {
+ $ret['extension'] = $pathinfo[5];
+ }
+ if (array_key_exists(3, $pathinfo)) {
+ $ret['filename'] = $pathinfo[3];
+ }
+ }
+ switch ($options) {
+ case PATHINFO_DIRNAME:
+ case 'dirname':
+ return $ret['dirname'];
+ case PATHINFO_BASENAME:
+ case 'basename':
+ return $ret['basename'];
+ case PATHINFO_EXTENSION:
+ case 'extension':
+ return $ret['extension'];
+ case PATHINFO_FILENAME:
+ case 'filename':
+ return $ret['filename'];
+ default:
+ return $ret;
+ }
+ }
+
+ /**
+ * Set or reset instance properties.
+ * You should avoid this function - it's more verbose, less efficient, more error-prone and
+ * harder to debug than setting properties directly.
+ * Usage Example:
+ * `$mail->set('SMTPSecure', static::ENCRYPTION_STARTTLS);`
+ * is the same as:
+ * `$mail->SMTPSecure = static::ENCRYPTION_STARTTLS;`.
+ *
+ * @param string $name The property name to set
+ * @param mixed $value The value to set the property to
+ *
+ * @return bool
+ */
+ public function set($name, $value = '')
+ {
+ if (property_exists($this, $name)) {
+ $this->$name = $value;
+
+ return true;
+ }
+ $this->setError($this->lang('variable_set') . $name);
+
+ return false;
+ }
+
+ /**
+ * Strip newlines to prevent header injection.
+ *
+ * @param string $str
+ *
+ * @return string
+ */
+ public function secureHeader($str)
+ {
+ return trim(str_replace(["\r", "\n"], '', $str));
+ }
+
+ /**
+ * Normalize line breaks in a string.
+ * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
+ * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
+ *
+ * @param string $text
+ * @param string $breaktype What kind of line break to use; defaults to static::$LE
+ *
+ * @return string
+ */
+ public static function normalizeBreaks($text, $breaktype = null)
+ {
+ if (null === $breaktype) {
+ $breaktype = static::$LE;
+ }
+ // Normalise to \n
+ $text = str_replace(["\r\n", "\r"], "\n", $text);
+ // Now convert LE as needed
+ if ("\n" !== $breaktype) {
+ $text = str_replace("\n", $breaktype, $text);
+ }
+
+ return $text;
+ }
+
+ /**
+ * Return the current line break format string.
+ *
+ * @return string
+ */
+ public static function getLE()
+ {
+ return static::$LE;
+ }
+
+ /**
+ * Set the line break format string, e.g. "\r\n".
+ *
+ * @param string $le
+ */
+ protected static function setLE($le)
+ {
+ static::$LE = $le;
+ }
+
+ /**
+ * Set the public and private key files and password for S/MIME signing.
+ *
+ * @param string $cert_filename
+ * @param string $key_filename
+ * @param string $key_pass Password for private key
+ * @param string $extracerts_filename Optional path to chain certificate
+ */
+ public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')
+ {
+ $this->sign_cert_file = $cert_filename;
+ $this->sign_key_file = $key_filename;
+ $this->sign_key_pass = $key_pass;
+ $this->sign_extracerts_file = $extracerts_filename;
+ }
+
+ /**
+ * Quoted-Printable-encode a DKIM header.
+ *
+ * @param string $txt
+ *
+ * @return string
+ */
+ public function DKIM_QP($txt)
+ {
+ $line = '';
+ $len = strlen($txt);
+ for ($i = 0; $i < $len; ++$i) {
+ $ord = ord($txt[$i]);
+ if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) {
+ $line .= $txt[$i];
+ } else {
+ $line .= '=' . sprintf('%02X', $ord);
+ }
+ }
+
+ return $line;
+ }
+
+ /**
+ * Generate a DKIM signature.
+ *
+ * @param string $signHeader
+ *
+ * @throws Exception
+ *
+ * @return string The DKIM signature value
+ */
+ public function DKIM_Sign($signHeader)
+ {
+ if (!defined('PKCS7_TEXT')) {
+ if ($this->exceptions) {
+ throw new Exception($this->lang('extension_missing') . 'openssl');
+ }
+
+ return '';
+ }
+ $privKeyStr = !empty($this->DKIM_private_string) ?
+ $this->DKIM_private_string :
+ file_get_contents($this->DKIM_private);
+ if ('' !== $this->DKIM_passphrase) {
+ $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
+ } else {
+ $privKey = openssl_pkey_get_private($privKeyStr);
+ }
+ if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
+ openssl_pkey_free($privKey);
+
+ return base64_encode($signature);
+ }
+ openssl_pkey_free($privKey);
+
+ return '';
+ }
+
+ /**
+ * Generate a DKIM canonicalization header.
+ * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.
+ * Canonicalized headers should *always* use CRLF, regardless of mailer setting.
+ *
+ * @see https://tools.ietf.org/html/rfc6376#section-3.4.2
+ *
+ * @param string $signHeader Header
+ *
+ * @return string
+ */
+ public function DKIM_HeaderC($signHeader)
+ {
+ //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]`
+ //@see https://tools.ietf.org/html/rfc5322#section-2.2
+ //That means this may break if you do something daft like put vertical tabs in your headers.
+ //Unfold header lines
+ $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
+ //Break headers out into an array
+ $lines = explode("\r\n", $signHeader);
+ foreach ($lines as $key => $line) {
+ //If the header is missing a :, skip it as it's invalid
+ //This is likely to happen because the explode() above will also split
+ //on the trailing LE, leaving an empty line
+ if (strpos($line, ':') === false) {
+ continue;
+ }
+ list($heading, $value) = explode(':', $line, 2);
+ //Lower-case header name
+ $heading = strtolower($heading);
+ //Collapse white space within the value, also convert WSP to space
+ $value = preg_replace('/[ \t]+/', ' ', $value);
+ //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value
+ //But then says to delete space before and after the colon.
+ //Net result is the same as trimming both ends of the value.
+ //By elimination, the same applies to the field name
+ $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t");
+ }
+
+ return implode("\r\n", $lines);
+ }
+
+ /**
+ * Generate a DKIM canonicalization body.
+ * Uses the 'simple' algorithm from RFC6376 section 3.4.3.
+ * Canonicalized bodies should *always* use CRLF, regardless of mailer setting.
+ *
+ * @see https://tools.ietf.org/html/rfc6376#section-3.4.3
+ *
+ * @param string $body Message Body
+ *
+ * @return string
+ */
+ public function DKIM_BodyC($body)
+ {
+ if (empty($body)) {
+ return "\r\n";
+ }
+ // Normalize line endings to CRLF
+ $body = static::normalizeBreaks($body, "\r\n");
+
+ //Reduce multiple trailing line breaks to a single one
+ return rtrim($body, "\r\n") . "\r\n";
+ }
+
+ /**
+ * Create the DKIM header and body in a new message header.
+ *
+ * @param string $headers_line Header lines
+ * @param string $subject Subject
+ * @param string $body Body
+ *
+ * @throws Exception
+ *
+ * @return string
+ */
+ public function DKIM_Add($headers_line, $subject, $body)
+ {
+ $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms
+ $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization methods of header & body
+ $DKIMquery = 'dns/txt'; // Query method
+ $DKIMtime = time();
+ //Always sign these headers without being asked
+ $autoSignHeaders = [
+ 'From',
+ 'To',
+ 'CC',
+ 'Date',
+ 'Subject',
+ 'Reply-To',
+ 'Message-ID',
+ 'Content-Type',
+ 'Mime-Version',
+ 'X-Mailer',
+ ];
+ if (stripos($headers_line, 'Subject') === false) {
+ $headers_line .= 'Subject: ' . $subject . static::$LE;
+ }
+ $headerLines = explode(static::$LE, $headers_line);
+ $currentHeaderLabel = '';
+ $currentHeaderValue = '';
+ $parsedHeaders = [];
+ $headerLineIndex = 0;
+ $headerLineCount = count($headerLines);
+ foreach ($headerLines as $headerLine) {
+ $matches = [];
+ if (preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) {
+ if ($currentHeaderLabel !== '') {
+ //We were previously in another header; This is the start of a new header, so save the previous one
+ $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
+ }
+ $currentHeaderLabel = $matches[1];
+ $currentHeaderValue = $matches[2];
+ } elseif (preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) {
+ //This is a folded continuation of the current header, so unfold it
+ $currentHeaderValue .= ' ' . $matches[1];
+ }
+ ++$headerLineIndex;
+ if ($headerLineIndex >= $headerLineCount) {
+ //This was the last line, so finish off this header
+ $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
+ }
+ }
+ $copiedHeaders = [];
+ $headersToSignKeys = [];
+ $headersToSign = [];
+ foreach ($parsedHeaders as $header) {
+ //Is this header one that must be included in the DKIM signature?
+ if (in_array($header['label'], $autoSignHeaders, true)) {
+ $headersToSignKeys[] = $header['label'];
+ $headersToSign[] = $header['label'] . ': ' . $header['value'];
+ if ($this->DKIM_copyHeaderFields) {
+ $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
+ str_replace('|', '=7C', $this->DKIM_QP($header['value']));
+ }
+ continue;
+ }
+ //Is this an extra custom header we've been asked to sign?
+ if (in_array($header['label'], $this->DKIM_extraHeaders, true)) {
+ //Find its value in custom headers
+ foreach ($this->CustomHeader as $customHeader) {
+ if ($customHeader[0] === $header['label']) {
+ $headersToSignKeys[] = $header['label'];
+ $headersToSign[] = $header['label'] . ': ' . $header['value'];
+ if ($this->DKIM_copyHeaderFields) {
+ $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
+ str_replace('|', '=7C', $this->DKIM_QP($header['value']));
+ }
+ //Skip straight to the next header
+ continue 2;
+ }
+ }
+ }
+ }
+ $copiedHeaderFields = '';
+ if ($this->DKIM_copyHeaderFields && count($copiedHeaders) > 0) {
+ //Assemble a DKIM 'z' tag
+ $copiedHeaderFields = ' z=';
+ $first = true;
+ foreach ($copiedHeaders as $copiedHeader) {
+ if (!$first) {
+ $copiedHeaderFields .= static::$LE . ' |';
+ }
+ //Fold long values
+ if (strlen($copiedHeader) > self::STD_LINE_LENGTH - 3) {
+ $copiedHeaderFields .= substr(
+ chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, static::$LE . ' '),
+ 0,
+ -strlen(static::$LE . ' ')
+ );
+ } else {
+ $copiedHeaderFields .= $copiedHeader;
+ }
+ $first = false;
+ }
+ $copiedHeaderFields .= ';' . static::$LE;
+ }
+ $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE;
+ $headerValues = implode(static::$LE, $headersToSign);
+ $body = $this->DKIM_BodyC($body);
+ $DKIMlen = strlen($body); // Length of body
+ $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body
+ $ident = '';
+ if ('' !== $this->DKIM_identity) {
+ $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE;
+ }
+ //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag
+ //which is appended after calculating the signature
+ //https://tools.ietf.org/html/rfc6376#section-3.5
+ $dkimSignatureHeader = 'DKIM-Signature: v=1;' .
+ ' d=' . $this->DKIM_domain . ';' .
+ ' s=' . $this->DKIM_selector . ';' . static::$LE .
+ ' a=' . $DKIMsignatureType . ';' .
+ ' q=' . $DKIMquery . ';' .
+ ' l=' . $DKIMlen . ';' .
+ ' t=' . $DKIMtime . ';' .
+ ' c=' . $DKIMcanonicalization . ';' . static::$LE .
+ $headerKeys .
+ $ident .
+ $copiedHeaderFields .
+ ' bh=' . $DKIMb64 . ';' . static::$LE .
+ ' b=';
+ //Canonicalize the set of headers
+ $canonicalizedHeaders = $this->DKIM_HeaderC(
+ $headerValues . static::$LE . $dkimSignatureHeader
+ );
+ $signature = $this->DKIM_Sign($canonicalizedHeaders);
+ $signature = trim(chunk_split($signature, self::STD_LINE_LENGTH - 3, static::$LE . ' '));
+
+ return static::normalizeBreaks($dkimSignatureHeader . $signature) . static::$LE;
+ }
+
+ /**
+ * Detect if a string contains a line longer than the maximum line length
+ * allowed by RFC 2822 section 2.1.1.
+ *
+ * @param string $str
+ *
+ * @return bool
+ */
+ public static function hasLineLongerThanMax($str)
+ {
+ return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str);
+ }
+
+ /**
+ * Allows for public read access to 'to' property.
+ * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+ *
+ * @return array
+ */
+ public function getToAddresses()
+ {
+ return $this->to;
+ }
+
+ /**
+ * Allows for public read access to 'cc' property.
+ * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+ *
+ * @return array
+ */
+ public function getCcAddresses()
+ {
+ return $this->cc;
+ }
+
+ /**
+ * Allows for public read access to 'bcc' property.
+ * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+ *
+ * @return array
+ */
+ public function getBccAddresses()
+ {
+ return $this->bcc;
+ }
+
+ /**
+ * Allows for public read access to 'ReplyTo' property.
+ * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+ *
+ * @return array
+ */
+ public function getReplyToAddresses()
+ {
+ return $this->ReplyTo;
+ }
+
+ /**
+ * Allows for public read access to 'all_recipients' property.
+ * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+ *
+ * @return array
+ */
+ public function getAllRecipientAddresses()
+ {
+ return $this->all_recipients;
+ }
+
+ /**
+ * Perform a callback.
+ *
+ * @param bool $isSent
+ * @param array $to
+ * @param array $cc
+ * @param array $bcc
+ * @param string $subject
+ * @param string $body
+ * @param string $from
+ * @param array $extra
+ */
+ protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra)
+ {
+ if (!empty($this->action_function) && is_callable($this->action_function)) {
+ call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra);
+ }
+ }
+
+ /**
+ * Get the OAuth instance.
+ *
+ * @return OAuth
+ */
+ public function getOAuth()
+ {
+ return $this->oauth;
+ }
+
+ /**
+ * Set an OAuth instance.
+ */
+ public function setOAuth(OAuth $oauth)
+ {
+ $this->oauth = $oauth;
+ }
+}
diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/README.md b/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/README.md
new file mode 100644
index 0000000..c2f7960
--- /dev/null
+++ b/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/README.md
@@ -0,0 +1,95 @@
+### 使用PHP发送邮件
+
+#### 依赖composer包:wanghua/general-utility-tools-php
+#### 环境要求:PHP7+, ThinkPHP5+
+
+示例:
+
+步骤1:填写配置=>config
+
+ 说明:根据需要读取数据库或文件配置
+
+步骤2,调用:
+
+```
+//原生方式(复制可用)
+Logger::send('test title','test body');
+```
+
+```
+//调用封装类(复制可用)
+Logger::open_debug();//打开调试模式
+Logger::log(['error'=>'系统错误'],'test_log');//调用日志写入
+```
+
+#### 封装类:
+```
+
+/**
+(复制可用)
+ * Class Logger
+ * @package app\common\tools
+ */
+class Logger
+{
+
+ /**
+ * desc:开启调试模式
+ * author:wh
+ */
+ static function open_debug(){
+ LoggerObj::open_debug();
+ }
+
+ /**
+ * desc:关闭调试模式
+ * author:wh
+ */
+ static function close_debug(){
+ LoggerObj::close_debug();
+ }
+ /**
+ * desc:系统未来通用统一日志记录
+ * author:wh
+ * @param array $data
+ * @param string $file_log_name
+ */
+ static function log(array $data=[], string $file_log_name){
+ //服务器配置 start
+ $send_server_nickname = SundryConfigModel::getConfigVal('send_server_username');
+ $send_server_username = SundryConfigModel::getConfigVal('send_server_username');
+ $send_server_pwd = SundryConfigModel::getConfigVal('send_server_pwd');
+ $send_server_host = SundryConfigModel::getConfigVal('send_server_host');
+ //服务器配置 end
+
+ //收件人信息 start
+ $receiver = SundryConfigModel::getConfigVal('admin_error_log_email');
+ $receiver_nickname = '日志管理员';//默认收件人昵称
+ //收件人信息 end
+
+ //写入日志
+ LoggerObj::set_email_config($send_server_nickname,$send_server_username,$send_server_pwd,$send_server_host);
+ LoggerObj::set_receiver_config($receiver,$receiver_nickname);
+ LoggerObj::write_text($data,$file_log_name);
+ }
+}
+```
+
+#### 原生方式:
+```
+class Logger{
+ //(复制可用)
+ static function send($title, $body){
+ $host = 'smtp.qq.com';//qq邮件发送服务器
+ $nickname = 'test风控';//你的邮件发送服务器昵称
+ $username = '1003076666@qq.com';//你的邮件发送服务器账号
+ $userpass = 'pdosdyjuurowbcfc';//你的邮件发送服务器密码
+ //邮件通知
+ $mail = new Mail($nickname, $host, $username, $userpass);
+ $mail->debug = false;//true 测试模式
+ $agent = Db::table('fa_agent')->where(['id'=>1])->find();//查询数据
+
+ return $mail->send($agent['email'], $agent['nickname'], $title,$body);
+ }
+}
+```
\ No newline at end of file
diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/SMTP.php b/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/SMTP.php
new file mode 100644
index 0000000..c1aeb1c
--- /dev/null
+++ b/admin/vendor/wanghua/general-utility-tools-php/src/phpmailer/SMTP.php
@@ -0,0 +1,1370 @@
+
+ * @author Jim Jagielski (jimjag) `, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * ```php + * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * ``` + * + * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` + * level output is used: + * + * ```php + * $mail->Debugoutput = new myPsr3Logger; + * ``` + * + * @var string|callable|\Psr\Log\LoggerInterface + */ + public $Debugoutput = 'echo'; + + /** + * Whether to use VERP. + * + * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path + * @see http://www.postfix.org/VERP_README.html Info on VERP + * + * @var bool + */ + public $do_verp = false; + + /** + * The timeout value for connection, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. + * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. + * + * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2 + * + * @var int + */ + public $Timeout = 300; + + /** + * How long to wait for commands to complete, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. + * + * @var int + */ + public $Timelimit = 300; + + /** + * Patterns to extract an SMTP transaction id from reply to a DATA command. + * The first capture group in each regex will be used as the ID. + * MS ESMTP returns the message ID, which may not be correct for internal tracking. + * + * @var string[] + */ + protected $smtp_transaction_id_patterns = [ + 'exim' => '/[\d]{3} OK id=(.*)/', + 'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/', + 'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/', + 'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/', + 'Amazon_SES' => '/[\d]{3} Ok (.*)/', + 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/', + 'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/', + ]; + + /** + * The last transaction ID issued in response to a DATA command, + * if one was detected. + * + * @var string|bool|null + */ + protected $last_smtp_transaction_id; + + /** + * The socket for the server connection. + * + * @var ?resource + */ + protected $smtp_conn; + + /** + * Error information, if any, for the last SMTP command. + * + * @var array + */ + protected $error = [ + 'error' => '', + 'detail' => '', + 'smtp_code' => '', + 'smtp_code_ex' => '', + ]; + + /** + * The reply the server sent to us for HELO. + * If null, no HELO string has yet been received. + * + * @var string|null + */ + protected $helo_rply; + + /** + * The set of SMTP extensions sent in reply to EHLO command. + * Indexes of the array are extension names. + * Value at index 'HELO' or 'EHLO' (according to command that was sent) + * represents the server name. In case of HELO it is the only element of the array. + * Other values can be boolean TRUE or an array containing extension options. + * If null, no HELO/EHLO string has yet been received. + * + * @var array|null + */ + protected $server_caps; + + /** + * The most recent reply received from the server. + * + * @var string + */ + protected $last_reply = ''; + + /** + * Output debugging info via a user-selected method. + * + * @param string $str Debug string to output + * @param int $level The debug level of this message; see DEBUG_* constants + * + * @see SMTP::$Debugoutput + * @see SMTP::$do_debug + */ + protected function edebug($str, $level = 0) + { + if ($level > $this->do_debug) { + return; + } + //Is this a PSR-3 logger? + if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { + $this->Debugoutput->debug($str); + + return; + } + //Avoid clash with built-in function names + if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) { + call_user_func($this->Debugoutput, $str, $level); + + return; + } + switch ($this->Debugoutput) { + case 'error_log': + //Don't output, just log + error_log($str); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo gmdate('Y-m-d H:i:s'), ' ', htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ), " \n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace('/\r\n|\r/m', "\n", $str); + echo gmdate('Y-m-d H:i:s'), + "\t", + //Trim trailing space + trim( + //Indent for readability, except for trailing break + str_replace( + "\n", + "\n \t ", + trim($str) + ) + ), + "\n"; + } + } + + /** + * Connect to an SMTP server. + * + * @param string $host SMTP server IP or host name + * @param int $port The port number to connect to + * @param int $timeout How long to wait for the connection to open + * @param array $options An array of options for stream_context_create() + * + * @return bool + */ + public function connect($host, $port = null, $timeout = 30, $options = []) + { + static $streamok; + //This is enabled by default since 5.0.0 but some providers disable it + //Check this once and cache the result + if (null === $streamok) { + $streamok = function_exists('stream_socket_client'); + } + // Clear errors to avoid confusion + $this->setError(''); + // Make sure we are __not__ connected + if ($this->connected()) { + // Already connected, generate error + $this->setError('Already connected to a server'); + + return false; + } + if (empty($port)) { + $port = self::DEFAULT_PORT; + } + // Connect to the SMTP server + $this->edebug( + "Connection: opening to $host:$port, timeout=$timeout, options=" . + (count($options) > 0 ? var_export($options, true) : 'array()'), + self::DEBUG_CONNECTION + ); + $errno = 0; + $errstr = ''; + if ($streamok) { + $socket_context = stream_context_create($options); + set_error_handler([$this, 'errorHandler']); + $this->smtp_conn = stream_socket_client( + $host . ':' . $port, + $errno, + $errstr, + $timeout, + STREAM_CLIENT_CONNECT, + $socket_context + ); + restore_error_handler(); + } else { + //Fall back to fsockopen which should work in more places, but is missing some features + $this->edebug( + 'Connection: stream_socket_client not available, falling back to fsockopen', + self::DEBUG_CONNECTION + ); + set_error_handler([$this, 'errorHandler']); + $this->smtp_conn = fsockopen( + $host, + $port, + $errno, + $errstr, + $timeout + ); + restore_error_handler(); + } + // Verify we connected properly + if (!is_resource($this->smtp_conn)) { + $this->setError( + 'Failed to connect to server', + '', + (string) $errno, + $errstr + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] + . ": $errstr ($errno)", + self::DEBUG_CLIENT + ); + + return false; + } + $this->edebug('Connection: opened', self::DEBUG_CONNECTION); + // SMTP server can take longer to respond, give longer timeout for first read + // Windows does not have support for this timeout function + if (strpos(PHP_OS, 'WIN') !== 0) { + $max = (int) ini_get('max_execution_time'); + // Don't bother if unlimited + if (0 !== $max && $timeout > $max) { + @set_time_limit($timeout); + } + stream_set_timeout($this->smtp_conn, $timeout, 0); + } + // Get any announcement + $announce = $this->get_lines(); + $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER); + + return true; + } + + /** + * Initiate a TLS (encrypted) session. + * + * @return bool + */ + public function startTLS() + { + if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) { + return false; + } + + //Allow the best TLS version(s) we can + $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT; + + //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT + //so add them back in manually if we can + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + + // Begin encrypted connection + set_error_handler([$this, 'errorHandler']); + $crypto_ok = stream_socket_enable_crypto( + $this->smtp_conn, + true, + $crypto_method + ); + restore_error_handler(); + + return (bool) $crypto_ok; + } + + /** + * Perform SMTP authentication. + * Must be run after hello(). + * + * @see hello() + * + * @param string $username The user name + * @param string $password The password + * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2) + * @param OAuth $OAuth An optional OAuth instance for XOAUTH2 authentication + * + * @return bool True if successfully authenticated + */ + public function authenticate( + $username, + $password, + $authtype = null, + $OAuth = null + ) { + if (!$this->server_caps) { + $this->setError('Authentication is not allowed before HELO/EHLO'); + + return false; + } + + if (array_key_exists('EHLO', $this->server_caps)) { + // SMTP extensions are available; try to find a proper authentication method + if (!array_key_exists('AUTH', $this->server_caps)) { + $this->setError('Authentication is not allowed at this stage'); + // 'at this stage' means that auth may be allowed after the stage changes + // e.g. after STARTTLS + + return false; + } + + $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL); + $this->edebug( + 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']), + self::DEBUG_LOWLEVEL + ); + + //If we have requested a specific auth type, check the server supports it before trying others + if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) { + $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL); + $authtype = null; + } + + if (empty($authtype)) { + //If no auth mechanism is specified, attempt to use these, in this order + //Try CRAM-MD5 first as it's more secure than the others + foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) { + if (in_array($method, $this->server_caps['AUTH'], true)) { + $authtype = $method; + break; + } + } + if (empty($authtype)) { + $this->setError('No supported authentication methods found'); + + return false; + } + $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL); + } + + if (!in_array($authtype, $this->server_caps['AUTH'], true)) { + $this->setError("The requested authentication method \"$authtype\" is not supported by the server"); + + return false; + } + } elseif (empty($authtype)) { + $authtype = 'LOGIN'; + } + switch ($authtype) { + case 'PLAIN': + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) { + return false; + } + // Send encoded username and password + if (!$this->sendCommand( + 'User & Password', + base64_encode("\0" . $username . "\0" . $password), + 235 + ) + ) { + return false; + } + break; + case 'LOGIN': + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) { + return false; + } + if (!$this->sendCommand('Username', base64_encode($username), 334)) { + return false; + } + if (!$this->sendCommand('Password', base64_encode($password), 235)) { + return false; + } + break; + case 'CRAM-MD5': + // Start authentication + if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) { + return false; + } + // Get the challenge + $challenge = base64_decode(substr($this->last_reply, 4)); + + // Build the response + $response = $username . ' ' . $this->hmac($challenge, $password); + + // send encoded credentials + return $this->sendCommand('Username', base64_encode($response), 235); + case 'XOAUTH2': + //The OAuth instance must be set up prior to requesting auth. + if (null === $OAuth) { + return false; + } + $oauth = $OAuth->getOauth64(); + + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { + return false; + } + break; + default: + $this->setError("Authentication method \"$authtype\" is not supported"); + + return false; + } + + return true; + } + + /** + * Calculate an MD5 HMAC hash. + * Works like hash_hmac('md5', $data, $key) + * in case that function is not available. + * + * @param string $data The data to hash + * @param string $key The key to hash with + * + * @return string + */ + protected function hmac($data, $key) + { + if (function_exists('hash_hmac')) { + return hash_hmac('md5', $data, $key); + } + + // The following borrowed from + // http://php.net/manual/en/function.mhash.php#27225 + + // RFC 2104 HMAC implementation for php. + // Creates an md5 HMAC. + // Eliminates the need to install mhash to compute a HMAC + // by Lance Rushing + + $bytelen = 64; // byte length for md5 + if (strlen($key) > $bytelen) { + $key = pack('H*', md5($key)); + } + $key = str_pad($key, $bytelen, chr(0x00)); + $ipad = str_pad('', $bytelen, chr(0x36)); + $opad = str_pad('', $bytelen, chr(0x5c)); + $k_ipad = $key ^ $ipad; + $k_opad = $key ^ $opad; + + return md5($k_opad . pack('H*', md5($k_ipad . $data))); + } + + /** + * Check connection state. + * + * @return bool True if connected + */ + public function connected() + { + if (is_resource($this->smtp_conn)) { + $sock_status = stream_get_meta_data($this->smtp_conn); + if ($sock_status['eof']) { + // The socket is valid but we are not connected + $this->edebug( + 'SMTP NOTICE: EOF caught while checking if connected', + self::DEBUG_CLIENT + ); + $this->close(); + + return false; + } + + return true; // everything looks good + } + + return false; + } + + /** + * Close the socket and clean up the state of the class. + * Don't use this function without first trying to use QUIT. + * + * @see quit() + */ + public function close() + { + $this->setError(''); + $this->server_caps = null; + $this->helo_rply = null; + if (is_resource($this->smtp_conn)) { + // close the connection and cleanup + fclose($this->smtp_conn); + $this->smtp_conn = null; //Makes for cleaner serialization + $this->edebug('Connection: closed', self::DEBUG_CONNECTION); + } + } + + /** + * Send an SMTP DATA command. + * Issues a data command and sends the msg_data to the server, + * finializing the mail transaction. $msg_data is the message + * that is to be send with the headers. Each header needs to be + * on a single line followed by a 输入参数(json后): +$err_json_input +触发url地址: +$url +详细错误信息: +$error_info +EOF; + + //写入日志 + return $email_obj->write_text($title,$body); + }catch (\Exception $e){ + Tools::log_to_write_txt([ + 'error'=>'系统未来通用统一日志记录,出错.'.$e->getMessage(), + 'title'=>$title, + 'body'=>$error_info, + 'error_info'=>$e->getTraceAsString()],$log_file); + return ['code'=>500,'msg'=>'日志写入异常.'.$e->getMessage()]; + } + } + + /** + * desc:通过阿里云邮件服务器发送邮件 + * author:wh + */ + static function sendAliEmail($toAddress,$subject,$content){ + $config = config('aliyun_oss_config'); + return (new AliYunEmail($config))->sendMail($toAddress,$subject,$content); + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/tool/Ip.php b/admin/vendor/wanghua/general-utility-tools-php/src/tool/Ip.php new file mode 100644 index 0000000..ce7b494 --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/tool/Ip.php @@ -0,0 +1,274 @@ +isPrivateIP($ip)){ + return true;//国内 + } + switch ($type){ + case 'qifu_baidu': + return $this->qihuBaidu($ip); + break; + case 'ipapi': + return $this->ipApi($ip); + break; + default: + return $this->ipApi($ip); + } + } + + + /** + * desc:是否为私有IP + * author:wh + * @param $ip + * @return bool + */ + function isPrivateIP($ip) { + // 将IP地址字符串转换为整数 + $ipLong = ip2long($ip); + // 检查转换是否成功以及IP地址是否属于私有地址段 + if ($ipLong !== false) { + // A类私有地址 + if ($ipLong >= ip2long('10.0.0.0') && $ipLong <= ip2long('10.255.255.255')) { + return true; + } + // B类私有地址 + elseif ($ipLong >= ip2long('172.16.0.0') && $ipLong <= ip2long('172.31.255.255')) { + return true; + } + // C类私有地址 + elseif ($ipLong >= ip2long('192.168.0.0') && $ipLong <= ip2long('192.168.255.255')) { + return true; + } + } + // 如果不在私有地址范围内或转换失败,则不是私有IP + return false; + } + + /** + * desc:百度ip查询 + * + * author:wh + * 查询结果: + * {"code":"Success","data":{"continent":"亚洲","country":"中国","zipcode":"401120","timezone":"UTC+8", + * "accuracy":"区县","owner":"中国移动","isp":"中国移动","source":"数据挖掘","areacode":"CN","adcode":"500112", + * "asnumber":"9808","lat":"29.813215","lng":"106.743200","radius":"35.6842","prov":"重庆市","city":"重庆市", + * "district":"渝北区"},"charge":true,"msg":"查询成功","ip":"183.227.88.66","coordsys":"WGS84"} + * @param $ip + * @return bool + */ + function qihuBaidu($ip){ + if($this->isPrivateIP($ip)){ + return true;//国内 + } + if(cache($ip)){ + Tools::log_to_write_txt(['读取缓存ip地址:'.$ip,'$res(yes为国内)'=>cache($ip)]); + return cache($ip) == 'yes'; + } + //先走缓存 + if($this->getRecord($ip)){ + return false; + } + + + + $res = $this->curl_get($ip); + Tools::log_to_write_txt(['error'=>'IP查询,curl结束:'.$ip,'$res'=>$res]); + if($res['code'] == 200){//请求成功 + if(empty($res['data'])){ + Tools::log_to_write_txt(['error'=>'IP查询,curl结果为空','$res'=>$res]); + } + //校验ip属地 + $data = json_decode($res['data'],true); + if($data['code'] != 'Success'){//查询失败 + Tools::log_to_write_txt(['error'=>'IP查询成功,但结果返回为失败.','$res'=>$res]); + return true; + } + //查询成功 + if(empty($data['data']['country'])){ + //country为空时默认为国内 + Tools::log_to_write_txt(['error'=>'IP查询成功,但country返回为空.','$res'=>$res]); + return true; + } + if($data['data']['country'] == '中国'){ + //缓存ip地址 + cache($ip,'yes');//国内 + return true; + } + $this->insertRecord($ip,$data['data']['continent'].' '.$data['data']['prov'],$data['data']['country'],'no'); + //缓存ip地址 + cache($ip,'no');//国外 + Tools::log_to_write_txt(['ip地址不在中国内,禁止访问:'.$ip,'curl data'=>$res['data']]); + //return redirect($this->redirect_url); + return false; + } + return true; + } + /** + * ipapi查询IP + * + * 查询IP归属哪个国家(白嫖查询,不保证每次都能查询成功) + * + * CN表示中国,其它表示国外 + */ + function ipApi($ip){ + if($this->isPrivateIP($ip)){ + return true;//国内 + } + if(cache($ip)){ + Tools::log_to_write_txt(['读取缓存ip地址:'.$ip,'$res(yes为国内)'=>cache($ip)]); + return cache($ip) == 'yes'; + } + //先走缓存 + if($this->getRecord($ip)){ + return false; + } + /** cache($ip)为空时进行实时查询 */ + + $url = "https://ipapi.co/{$ip}/country/"; + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => '', + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 0, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_HTTPHEADER => array( + 'User-Agent: Apifox/1.0.0 (https://apifox.com)', + 'Accept: */*', + 'Host: ipapi.co', + 'Connection: keep-alive' + ), + )); + + $response = curl_exec($curl); + + curl_close($curl); + if($response == 'CN'){ + cache($ip,'yes');//国内 + return true; + } + $this->insertRecord($ip,$response,$response); + Tools::log_to_write_txt(['ip地址不在中国内,禁止访问:'.$ip,'$response'=>$response,'$url'=>$url]); + cache($ip,'no');//国外 + return false; + } + + /** + * desc: + * author:wh + * @param string $territory 属地 + * @param string $country 国家 + * @param string $is_china 是否中国 + */ + private function insertRecord($ip,$territory='',$country='',$is_china='no'){ + /** + + CREATE TABLE `fa_ip_attack_record` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', + `ip` varchar(30) NOT NULL DEFAULT '' COMMENT 'IP', + `territory` varchar(50) DEFAULT '' COMMENT '归属地', + `country` varchar(15) DEFAULT '' COMMENT '国家', + `is_china` varchar(10) NOT NULL DEFAULT '' COMMENT '是否中国', + `url` text NOT NULL COMMENT '带域名查询参数', + `params` text COMMENT '参数', + `header` text COMMENT '请求头', + `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '攻击时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `index_ip` (`ip`) USING BTREE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IP攻击记录'; + + */ + //IP攻击记录 + Db::table('fa_ip_attack_record') + ->insert([ + 'ip'=>$ip, + 'territory'=>$territory, + 'country'=>$country, + 'is_china'=>$is_china, + 'url'=>request()->url(true), + 'params'=>json_encode(input(),JSON_UNESCAPED_UNICODE), + 'header'=>json_encode(request()->header(),JSON_UNESCAPED_UNICODE), + ]); + } + + /** + * desc:查询IP攻击记录 + * author:wh + * @param $ip + */ + private function getRecord($ip){ + return Db::table('fa_ip_attack_record') + ->where('ip',$ip) + ->field('id,is_china') + ->find(); + } + + /** + * desc:curl + * author:wh + * @param $ip + * @return array + */ + function curl_get($ip){ + $curl = curl_init(); + + curl_setopt_array($curl, array( + CURLOPT_URL => 'https://qifu.baidu.com/ip/geo/v1/district?ip='.$ip, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => '', + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 0, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => 'CURL_HTTP_VERSION_1_1', + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_HTTPHEADER => array( + 'User-Agent: Apifox/1.0.0 (https://apifox.com)', + 'Accept: */*', + 'Host: qifu.baidu.com', + 'Connection: keep-alive' + ), + )); + + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE); + $data = curl_exec($curl); + + // 显示错误信息 + if (curl_error($curl)) { + //print "Error: ".curl_errno($curl).'-' . curl_error($curl); + //返回错误码 + return ['code' => curl_errno($curl), 'msg' => curl_error($curl)]; + } else { + //关闭句柄 + curl_close($curl); + // 返回的内容 + return ['code' => 200, 'msg' => 'cURL ok', 'data' => $data]; + } + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/tool/KuaidiTools.php b/admin/vendor/wanghua/general-utility-tools-php/src/tool/KuaidiTools.php new file mode 100644 index 0000000..f85b35e --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/tool/KuaidiTools.php @@ -0,0 +1,232 @@ +getPlatform($num); + if($plt['code'] != 200){ + return Tools::set_res(500,'查询失败.'.$plt['msg']); + } + $company = $plt['data']['company']; + $url = "https://alayn.baidu.com/express/appdetail/get_detail?query_from_srcid=51151&tokenV2=OLRFkDonXGP%2FQTmCKNu41zXJayuyUkfll0i7Tw5IWBFAsbTSXMycQooMPzAMO1Rc&appid=4001&nu={$num}&com={$company}&qid=4879176651996235000"; + $res = Curl::curl_get($url); + if($res['code'] != 200){ + return Tools::set_res($res['code'],'请求失败.'.$res['msg']); + } + $res_data = json_decode($res['data'],true); + if($res_data['error_code']){ + return Tools::set_res($res_data['error_code'],'查询失败.'.$res_data['msg']); + } + return Tools::set_ok('ok',$res_data['data']['context']); + }); + } + + + /** + * desc:查询爱查快递接口 + * { + "mailNo": "315002419145908:6033", + "status": 3, + "errCode": 0, + "message": "", + "expSpellName": "yunda", + "expTextName": "韵达快递", + "tel": "95546", + "data": [ + { + "time": "2024-06-18 18:45", + "context": "【潍坊市】山东潍坊奎文保税区公司-马维华(16602197439) 已揽收" + }, + { + "time": "2024-06-18 20:30", + "context": "【潍坊市】已到达 山东潍坊分拨交付中心" + }, + { + "time": "2024-06-18 20:35", + "context": "【潍坊市】已离开 山东潍坊分拨交付中心;发往 重庆分拨交付中心" + }, + { + "time": "2024-06-20 07:35", + "context": "【重庆市】已到达 重庆分拨交付中心" + }, + { + "time": "2024-06-20 07:55", + "context": "【重庆市】已离开 重庆分拨交付中心;发往 重庆渝北区西政公司" + }, + { + "time": "2024-06-20 13:35", + "context": "【重庆市】已离开 重庆市渝北区西政网格仓;发往 重庆渝北区西政公司" + }, + { + "time": "2024-06-20 13:35", + "context": "【重庆市】已到达 重庆渝北区西政公司[023-88972584]" + }, + { + "time": "2024-06-20 13:36", + "context": "【重庆市】重庆渝北区西政公司[023-88972584] 快递员 高邦明(13224040149) 正在为您派送【95121为韵达快递员外呼专属号码,请放心接听】" + }, + { + "time": "2024-06-20 13:36", + "context": "【重庆市】已到达 重庆渝北区西政公司[023-88972584]" + }, + { + "time": "2024-06-20 13:37", + "context": "【重庆市】重庆渝北区西政公司[023-88972584] 快递员 高邦明(13224040149) 正在为您派送【95121为韵达快递员外呼专属号码,请放心接听】" + }, + { + "time": "2024-06-20 14:25", + "context": "【代收点】您的快件已暂存至 兴科五路81妈妈驿站,地址:重庆市市辖区渝北区兴科五路81号,请及时领取签收,如有疑问请电联快递员:高邦明(13224040149) ,投诉电话:13274912700" + }, + { + "time": "2024-06-22 20:31", + "context": "【代收点】您的快件已签收,签收人在 兴科五路81妈妈驿站(重庆市市辖区渝北区兴科五路81号)领取,投诉电话:13274912700" + } + ], + "update": 1719293097, + "cache": 10576, + "lang": "zh", + "ord": "ASC" + } + * author:wh + * @param $num 快递单号 + * @param $phone_end4 手机后四位 + * @return array 返回data字段二维数组记录 + */ + function queryackd($num,$phone_end4){ + return Mmodel::catch(function () use($num,$phone_end4){ + $ran_time = mt_rand(1000000,2000000); + $tm = Tools::getMillisecond(); + $_ = $tm+$ran_time; + usleep($ran_time);//随机暂停1-2秒(伪装) + $tk = Tools::rand_str(); + + //快递平台 + $plt = $this->getPlatform($num); + if($plt['code'] != 200){ + return Tools::set_res(500,'查询失败.'.$plt['msg']); + } + $company = $plt['data']['company']; + + $url = "https://trace.fkdex.com/{$company}/{$num}:{$phone_end4}?mailNo=315002419145908%3A6033&spellName=&exp-textName=&tk={$tk}&tm={$tm}&_={$_}"; + $res = Curl::curl_get($url); + if($res['code'] != 200){ + return Tools::set_res($res['code'],'查询失败.'.$res['msg']); + } + if(empty($res['data'])){ + return Tools::set_fail('未查询到数据'); + } + $res_data = json_decode($res['data'],true); + if($res_data['errCode']){ + return Tools::set_res($res_data['errCode'],'查询失败.'.$res_data['message']); + } + return Tools::set_ok('ok',$res_data['data']); + }); + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/tool/MySqlTools.php b/admin/vendor/wanghua/general-utility-tools-php/src/tool/MySqlTools.php new file mode 100644 index 0000000..23561cb --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/tool/MySqlTools.php @@ -0,0 +1,78 @@ +size; + + //调用方法成功后,会在相应文件夹下生成二维码文件 + \PHPQRCode\QRcode::png($str, $outfile, $size); + + //$QR调用png生成的二维码全路径 + $QR = Tools::get_root_path().'public/uploads/qr_images/'.$qrname; + //$header头像全路径:头像一定是正方形 + $header = $logo; + $QR = \PHPQRCode\QRcode::addHeader($header, $QR); + + return $outfile; + } +} \ No newline at end of file diff --git a/admin/vendor/wanghua/general-utility-tools-php/src/tool/Tools.php b/admin/vendor/wanghua/general-utility-tools-php/src/tool/Tools.php new file mode 100644 index 0000000..af77bca --- /dev/null +++ b/admin/vendor/wanghua/general-utility-tools-php/src/tool/Tools.php @@ -0,0 +1,2560 @@ + $code, + 'msg' => $message, + 'data' => $data, + ]; + + if (is_array($code)) { + $r = array_merge($code, $r); + } + + if ($log) { + $dirname = 'result_log'; + if (is_array($log)) { + $dirname = isset($log['dirname']) ? $log['dirname'] : $dirname;//存储在runtime/log/下面 + } + $root_path = explode('vendor',__DIR__)[0]; + $filepath = $root_path . 'runtime/log/' . ($dirname). '/'.date('Ymd');//运行时日志 + if (!file_exists($filepath)) { + mkdir($filepath, 0777, true); + } + + $filepath .= '/log' . date('YmdH') . '.txt'; + if(!file_exists($filepath)){ + //创建 + touch($filepath); + // 设置文件权限 + chmod($filepath, 0777); + } + $str = "\n" . date('Y-m-d H:i:s') . ' | ' . request()->baseUrl() . "\n"; + + $str .= 'PARAMS: ';//参数 + $str .= json_encode(input(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"; + + $str .= 'RESULT: ';//结果 + $str .= json_encode($r, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ' | '; + + file_put_contents($filepath, $str, FILE_APPEND); + } + return $json ? json_encode($r, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : $r; + } + + /** + * desc:操作失败 + * author:wh + * @param string $msg 错误信息 + * @param array $data 返回数据 + * @param string $action 操作标识、控制器具体操作方法 + * @return array + */ + static function set_ok($msg='ok',$data=[],$action=''){ + if(is_array($msg)){ + $data = $msg; + $msg = 'ok'; + } + return ['code'=>200,'msg'=>$msg,'action'=>$action,'data'=>$data]; + } + + /** + * desc:操作失败 + * + * author:wh + * @param string $msg 错误信息 + * @param array $data 返回数据 + * @param string $action 操作标识、控制器具体操作方法 + * @return array + */ + static function set_fail($msg='操作失败',$data=[],$action=''){ + return ['code'=>500,'msg'=>$msg,'action'=>$action,'data'=>$data]; + } + + /** + * 设置event-stream的响应成功结果 + * @param string $msg 错误信息 + */ + static function set_event_stream_ok($msg='ok',$data=[],$action=''){ + //数据量太大时不能使用JSON_UNESCAPED_UNICODE + $json_data = json_encode(['code'=>200,'msg'=>$msg,'action'=>$action,'data'=>$data], JSON_UNESCAPED_UNICODE); + return <<'; + echo $msg; + echo ' '; + } + + + /** + * 数组转xml字符串 + * @throws + **/ + static function to_xml($data) + { + if (!is_array($data) || count($data) <= 0) { + throw new \Exception("数组数据异常!"); + } + + $xml = "
+ $strData
+
+ |