From 923cdc5e901f4e114b7ff4d1ca25c71d9766d92a Mon Sep 17 00:00:00 2001 From: net909 Date: Thu, 18 Apr 2024 19:35:14 +0800 Subject: [PATCH] 1.2 --- app/command/Dmtask.php | 76 ++++++++ app/common.php | 45 ++++- app/controller/Dmonitor.php | 261 +++++++++++++++++++++++++++ app/controller/Domain.php | 31 +++- app/controller/Index.php | 21 ++- app/lib/CheckUtils.php | 76 ++++++++ app/lib/DnsHelper.php | 15 +- app/lib/MsgNotice.php | 105 +++++++++++ app/lib/NewDb.php | 17 ++ app/lib/NewDbManager.php | 25 +++ app/lib/TaskRunner.php | 111 ++++++++++++ app/lib/dns/aliyun.php | 2 +- app/lib/dns/baidu.php | 7 +- app/lib/dns/cloudflare.php | 12 +- app/lib/dns/dnsla.php | 7 +- app/lib/dns/dnspod.php | 7 +- app/lib/dns/huawei.php | 11 +- app/lib/dns/west.php | 73 +++----- app/lib/mail/Aliyun.php | 72 ++++++++ app/lib/mail/PHPMailer/Exception.php | 2 + app/lib/mail/PHPMailer/PHPMailer.php | 2 + app/lib/mail/PHPMailer/SMTP.php | 2 + app/lib/mail/Sendcloud.php | 37 ++++ app/middleware/LoadConfig.php | 11 +- app/sql/install.sql | 53 ++++++ app/sql/update.sql | 43 +++++ app/view/common/layout.html | 17 +- app/view/dmonitor/noticeset.html | 170 +++++++++++++++++ app/view/dmonitor/overview.html | 155 ++++++++++++++++ app/view/dmonitor/task.html | 169 +++++++++++++++++ app/view/dmonitor/taskform.html | 242 +++++++++++++++++++++++++ app/view/dmonitor/taskinfo.html | 73 ++++++++ composer.json | 3 +- config/app.php | 4 +- config/console.php | 1 + config/database.php | 2 +- public/index.php | 4 + route/app.php | 12 ++ 38 files changed, 1889 insertions(+), 87 deletions(-) create mode 100644 app/command/Dmtask.php create mode 100644 app/controller/Dmonitor.php create mode 100644 app/lib/CheckUtils.php create mode 100644 app/lib/MsgNotice.php create mode 100644 app/lib/NewDb.php create mode 100644 app/lib/NewDbManager.php create mode 100644 app/lib/TaskRunner.php create mode 100644 app/lib/mail/Aliyun.php create mode 100644 app/lib/mail/PHPMailer/Exception.php create mode 100644 app/lib/mail/PHPMailer/PHPMailer.php create mode 100644 app/lib/mail/PHPMailer/SMTP.php create mode 100644 app/lib/mail/Sendcloud.php create mode 100644 app/sql/update.sql create mode 100644 app/view/dmonitor/noticeset.html create mode 100644 app/view/dmonitor/overview.html create mode 100644 app/view/dmonitor/task.html create mode 100644 app/view/dmonitor/taskform.html create mode 100644 app/view/dmonitor/taskinfo.html diff --git a/app/command/Dmtask.php b/app/command/Dmtask.php new file mode 100644 index 0000000..b602823 --- /dev/null +++ b/app/command/Dmtask.php @@ -0,0 +1,76 @@ +setName('dmtask') + ->setDescription('容灾切换任务'); + } + + protected function execute(Input $input, Output $output) + { + config_set('run_error', ''); + if(!extension_loaded('swoole')){ + $output->writeln('[Error] 未安装Swoole扩展'); + config_set('run_error', '未安装Swoole扩展'); + return; + } + try{ + $output->writeln('进程启动成功.'); + $this->runtask(); + }catch(Exception $e){ + $output->writeln('[Error] '.$e->getMessage()); + config_set('run_error', $e->getMessage()); + } + } + + private function runtask(){ + \Co::set(['hook_flags'=> SWOOLE_HOOK_ALL]); + \Co\run(function() { + $date = date("Ymd"); + $count = config_get('run_count', null, true) ?? 0; + while(true){ + sleep(1); + if($date != date("Ymd")){ + $count = 0; + $date = date("Ymd"); + } + + $rows = Db::name('dmtask')->where('checknexttime', '<=', time())->where('active', 1)->order('id', 'ASC')->select(); + foreach($rows as $row){ + \go(function () use($row) { + try{ + (new TaskRunner())->execute($row); + } catch (\Swoole\ExitException $e) { + echo $e->getStatus()."\n"; + } catch (Exception $e) { + echo $e->__toString()."\n"; + } + }); + Db::name('dmtask')->where('id', $row['id'])->update([ + 'checktime' => time(), + 'checknexttime' => time() + $row['frequency'] + ]); + $count++; + } + + config_set('run_time', date("Y-m-d H:i:s")); + config_set('run_count', $count); + } + }); + } +} diff --git a/app/common.php b/app/common.php index 50886fd..7ed61d8 100644 --- a/app/common.php +++ b/app/common.php @@ -201,10 +201,53 @@ function checkPermission($type, $domain = null){ function getAdminSkin(){ $skin = cookie('admin_skin'); if(empty($skin)){ - $skin = cache('admin_skin'); + $skin = config_get('admin_skin'); } if(empty($skin)){ $skin = 'skin-black-blue'; } return $skin; +} + +function config_get($key, $default = null, $force = false) +{ + if ($force) { + $value = Db::name('config')->where('key', $key)->value('value'); + } else { + $value = config('sys.'.$key); + } + return $value ?: $default; +} + +function config_set($key, $value) +{ + $res = Db::name('config')->replace()->insert(['key'=>$key, 'value'=>$value]); + return $res!==false; +} + +function getMillisecond() +{ + list($s1, $s2) = explode(' ', microtime()); + return (int)sprintf('%.0f', (floatval($s1) + floatval($s2)) * 1000); +} + +function getDnsType($value){ + if(filter_var($value, FILTER_VALIDATE_IP))return 'A'; + else return 'CNAME'; +} + +function convert_second($s){ + $m = floor($s/60); + if($m == 0){ + return $s.'秒'; + }else{ + $s = $s%60; + $h = floor($m/60); + if($h == 0){ + return $m.'分钟'.$s.'秒'; + }else{ + $m = $m%60; + return $h.'小时'.$m.'分钟'.$s.'秒'; + } + } } \ No newline at end of file diff --git a/app/controller/Dmonitor.php b/app/controller/Dmonitor.php new file mode 100644 index 0000000..a81fc11 --- /dev/null +++ b/app/controller/Dmonitor.php @@ -0,0 +1,261 @@ +alert('error', '无权限'); + $switch_count = Db::name('dmlog')->where('date', '>=', date("Y-m-d H:i:s",strtotime("-1 days")))->count(); + $fail_count = Db::name('dmlog')->where('date', '>=', date("Y-m-d H:i:s",strtotime("-1 days")))->where('action', 1)->count(); + + $run_state = config_get('run_time') ? (time()-strtotime(config_get('run_time')) > 10 ? 0 : 1) : 0; + View::assign('info', [ + 'run_count' => config_get('run_count', null, true) ?? 0, + 'run_time' => config_get('run_time', null, true) ?? '无', + 'run_state' => $run_state, + 'run_error' => config_get('run_error', null, true), + 'switch_count' => $switch_count, + 'fail_count' => $fail_count, + 'swoole' => extension_loaded('swoole') ? '已安装' : '未安装', + ]); + return View::fetch(); + } + + public function task() + { + if(!checkPermission(2)) return $this->alert('error', '无权限'); + return View::fetch(); + } + + public function task_data(){ + if(!checkPermission(2)) return json(['total'=>0, 'rows'=>[]]); + $type = input('post.type/d', 1); + $kw = input('post.kw', null, 'trim'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + + $select = Db::name('dmtask')->alias('A')->join('domain B','A.did = B.id'); + if(!empty($kw)){ + if($type == 1){ + $select->whereLike('rr|B.name', '%'.$kw.'%'); + }elseif($type == 2){ + $select->where('recordid', $kw); + }elseif($type == 3){ + $select->where('main_value', $kw); + }elseif($type == 4){ + $select->where('backup_value', $kw); + }elseif($type == 5){ + $select->whereLike('remark', '%'.$kw.'%'); + } + } + $total = $select->count(); + $list = $select->order('A.id','desc')->limit($offset, $limit)->field('A.*,B.name domain')->select()->toArray(); + + foreach($list as &$row){ + $row['checktimestr'] = date('Y-m-d H:i:s', $row['checktime']); + } + + return json(['total'=>$total, 'rows'=>$list]); + } + + public function taskform() + { + if(!checkPermission(2)) return $this->alert('error', '无权限'); + $action = input('param.action'); + if(request()->isPost()){ + if($action == 'add'){ + $task = [ + 'did' => input('post.did/d'), + 'rr' => input('post.rr', null, 'trim'), + 'recordid' => input('post.recordid', null, 'trim'), + 'type' => input('post.type/d'), + 'main_value' => input('post.main_value', null, 'trim'), + 'backup_value' => input('post.backup_value', null, 'trim'), + 'checktype' => input('post.checktype/d'), + 'checkurl' => input('post.checkurl', null, 'trim'), + 'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null, + 'frequency' => input('post.frequency/d'), + 'cycle' => input('post.cycle/d'), + 'timeout' => input('post.timeout/d'), + 'remark' => input('post.remark', null, 'trim'), + 'recordinfo' => input('post.recordinfo', null, 'trim'), + 'addtime' => time(), + 'active' => 1 + ]; + + if(empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])){ + return json(['code'=>-1, 'msg'=>'必填项不能为空']); + } + if($task['checktype'] > 0 && $task['timeout'] > $task['frequency']){ + return json(['code'=>-1, 'msg'=>'为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']); + } + if($task['type'] == 2 && $task['backup_value'] == $task['main_value']){ + return json(['code'=>-1, 'msg'=>'主备地址不能相同']); + } + if(Db::name('dmtask')->where('recordid', $task['recordid'])->find()){ + return json(['code'=>-1, 'msg'=>'当前容灾切换策略已存在']); + } + Db::name('dmtask')->insert($task); + return json(['code'=>0, 'msg'=>'添加成功']); + }elseif($action == 'edit'){ + $id = input('post.id/d'); + $task = [ + 'did' => input('post.did/d'), + 'rr' => input('post.rr', null, 'trim'), + 'recordid' => input('post.recordid', null, 'trim'), + 'type' => input('post.type/d'), + 'main_value' => input('post.main_value', null, 'trim'), + 'backup_value' => input('post.backup_value', null, 'trim'), + 'checktype' => input('post.checktype/d'), + 'checkurl' => input('post.checkurl', null, 'trim'), + 'tcpport' => !empty(input('post.tcpport')) ? input('post.tcpport/d') : null, + 'frequency' => input('post.frequency/d'), + 'cycle' => input('post.cycle/d'), + 'timeout' => input('post.timeout/d'), + 'remark' => input('post.remark', null, 'trim'), + 'recordinfo' => input('post.recordinfo', null, 'trim'), + ]; + + if(empty($task['did']) || empty($task['rr']) || empty($task['recordid']) || empty($task['main_value']) || empty($task['frequency']) || empty($task['cycle'])){ + return json(['code'=>-1, 'msg'=>'必填项不能为空']); + } + if($task['checktype'] > 0 && $task['timeout'] > $task['frequency']){ + return json(['code'=>-1, 'msg'=>'为保障容灾切换任务正常运行,最大超时时间不能大于检测间隔']); + } + if($task['type'] == 2 && $task['backup_value'] == $task['main_value']){ + return json(['code'=>-1, 'msg'=>'主备地址不能相同']); + } + if(Db::name('dmtask')->where('recordid', $task['recordid'])->where('id', '<>', $id)->find()){ + return json(['code'=>-1, 'msg'=>'当前容灾切换策略已存在']); + } + Db::name('dmtask')->where('id', $id)->update($task); + return json(['code'=>0, 'msg'=>'修改成功']); + }elseif($action == 'setactive'){ + $id = input('post.id/d'); + $active = input('post.active/d'); + Db::name('dmtask')->where('id', $id)->update(['active'=>$active]); + return json(['code'=>0, 'msg'=>'设置成功']); + }elseif($action == 'del'){ + $id = input('post.id/d'); + Db::name('dmtask')->where('id', $id)->delete(); + Db::name('dmlog')->where('taskid', $id)->delete(); + return json(['code'=>0, 'msg'=>'删除成功']); + }else{ + return json(['code'=>-1, 'msg'=>'参数错误']); + } + } + $task = null; + if($action == 'edit'){ + $id = input('get.id/d'); + $task = Db::name('dmtask')->where('id', $id)->find(); + if(empty($task)) return $this->alert('error', '任务不存在'); + } + + $domains = []; + foreach(Db::name('domain')->select() as $row){ + $domains[$row['id']] = $row['name']; + } + View::assign('domains', $domains); + + View::assign('info', $task); + View::assign('action', $action); + View::assign('support_ping', function_exists('exec')?'1':'0'); + return View::fetch('taskform'); + } + + public function taskinfo() + { + if(!checkPermission(2)) return $this->alert('error', '无权限'); + $id = input('param.id/d'); + $task = Db::name('dmtask')->where('id', $id)->find(); + if(empty($task)) return $this->alert('error', '任务不存在'); + + $switch_count = Db::name('dmlog')->where('taskid', $id)->where('date', '>=', date("Y-m-d H:i:s",strtotime("-1 days")))->count(); + $fail_count = Db::name('dmlog')->where('taskid', $id)->where('date', '>=', date("Y-m-d H:i:s",strtotime("-1 days")))->where('action', 1)->count(); + + $task['switch_count'] = $switch_count; + $task['fail_count'] = $fail_count; + if($task['type'] == 2){ + $task['action_name'] = ['未知', '切换备用解析记录', '恢复主解析记录']; + }else{ + $task['action_name'] = ['未知', '暂停解析', '启用解析']; + } + View::assign('info', $task); + return View::fetch(); + } + + public function tasklog_data(){ + if(!checkPermission(2)) return json(['total'=>0, 'rows'=>[]]); + $taskid = input('param.id/d'); + $offset = input('post.offset/d'); + $limit = input('post.limit/d'); + $action = input('post.action/d', 0); + + $select = Db::name('dmlog')->where('taskid', $taskid); + if($action > 0){ + $select->where('action', $action); + } + $total = $select->count(); + $list = $select->order('id','desc')->limit($offset, $limit)->select(); + + return json(['total'=>$total, 'rows'=>$list]); + } + + public function noticeset() + { + if(!checkPermission(2)) return $this->alert('error', '无权限'); + if(request()->isPost()){ + $params = input('post.'); + if(isset($params['mail_type']) && isset($params['mail_name2']) && $params['mail_type'] > 0){ + $params['mail_name'] = $params['mail_name2']; + unset($params['mail_name2']); + } + foreach ($params as $key=>$value){ + if (empty($key)) { + continue; + } + config_set($key, $value); + } + return json(['code'=>0, 'msg'=>'succ']); + } + return View::fetch(); + } + + public function mailtest() + { + if(!checkPermission(2)) return $this->alert('error', '无权限'); + $mail_name = config_get('mail_recv')?config_get('mail_recv'):config_get('mail_name'); + if(empty($mail_name)) return json(['code'=>-1, 'msg'=>'您还未设置邮箱!']); + $result = \app\lib\MsgNotice::send_mail($mail_name,'邮件发送测试。','这是一封测试邮件!

来自:'.request()->root(true)); + if($result === true){ + return json(['code'=>0, 'msg'=>'邮件发送成功!']); + }else{ + return json(['code'=>-1, 'msg'=>'邮件发送失败!'.$result]); + } + } + + public function clean() + { + if(!checkPermission(2)) return $this->alert('error', '无权限'); + if(request()->isPost()){ + $days = input('post.days/d'); + if(!$days || $days < 0) return json(['code'=>-1, 'msg'=>'参数错误']); + Db::execute("DELETE FROM `".config('database.connections.mysql.prefix')."dmlog` WHERE `date`<'".date("Y-m-d H:i:s",strtotime("-".$days." days"))."'"); + Db::execute("OPTIMIZE TABLE `".config('database.connections.mysql.prefix')."dmlog`"); + return json(['code'=>0, 'msg'=>'清理成功']); + } + } + + public function status() + { + $run_state = config_get('run_time', null, true) ? (time()-strtotime(config_get('run_time')) > 10 ? 0 : 1) : 0; + return $run_state == 1 ? 'ok' : 'error'; + } +} \ No newline at end of file diff --git a/app/controller/Domain.php b/app/controller/Domain.php index 441f3c4..c5f5dc4 100644 --- a/app/controller/Domain.php +++ b/app/controller/Domain.php @@ -219,6 +219,7 @@ class Domain extends BaseController if(!checkPermission(2)) return $this->alert('error', '无权限'); $id = input('post.id/d'); Db::name('domain')->where('id', $id)->delete(); + Db::name('dmtask')->where('did', $id)->delete(); return json(['code'=>0]); } return json(['code'=>-3]); @@ -345,13 +346,35 @@ class Domain extends BaseController $recordLine = cache('record_line_'.$id); - $list = []; - foreach($domainRecords['list'] as $row){ + foreach($domainRecords['list'] as &$row){ $row['LineName'] = isset($recordLine[$row['Line']]) ? $recordLine[$row['Line']]['name'] : $row['Line']; - $list[] = $row; } - return json(['total'=>$domainRecords['total'], 'rows'=>$list]); + return json(['total'=>$domainRecords['total'], 'rows'=>$domainRecords['list']]); + } + + public function record_list(){ + if(!checkPermission(2)) return $this->alert('error', '无权限'); + $id = input('post.id/d'); + $rr = input('post.rr', null, 'trim'); + + $drow = Db::name('domain')->where('id', $id)->find(); + if(!$drow){ + return json(['code'=>-1, 'msg'=>'域名不存在']); + } + if(!checkPermission(0, $drow['name'])) return json(['code'=>-1, 'msg'=>'无权限']); + + $dns = DnsHelper::getModel($drow['aid'], $drow['name'], $drow['thirdid']); + $domainRecords = $dns->getSubDomainRecords($rr, 1, 100); + if(!$domainRecords) return json(['code'=>-1, 'msg'=>'获取记录列表失败,'.$dns->getError()]); + + list($recordLine, $minTTL) = $this->get_line_and_ttl($drow); + + foreach($domainRecords['list'] as &$row){ + $row['LineName'] = isset($recordLine[$row['Line']]) ? $recordLine[$row['Line']]['name'] : $row['Line']; + } + + return json(['code'=>0, 'data'=>$domainRecords['list']]); } public function record_add(){ diff --git a/app/controller/Index.php b/app/controller/Index.php index bf4a387..09612d5 100644 --- a/app/controller/Index.php +++ b/app/controller/Index.php @@ -32,6 +32,12 @@ class Index extends BaseController return json(['code'=>-3]); } + if(config('app.dbversion') && config_get('version') != config('app.dbversion')){ + $this->db_update(); + config_set('version', config('app.dbversion')); + Cache::clear(); + } + $tmp = 'version()'; $mysqlVersion = Db::query("select version()")[0][$tmp]; $info = [ @@ -47,13 +53,26 @@ class Index extends BaseController return view(); } + private function db_update(){ + $sqls=file_get_contents(app()->getAppPath().'sql/update.sql'); + $mysql_prefix = env('database.prefix', 'dnsmgr_'); + $sqls=explode(';', $sqls); + foreach ($sqls as $value) { + $value=trim($value); + if(empty($value))continue; + $value = str_replace('dnsmgr_',$mysql_prefix,$value); + Db::execute($value); + } + } + public function changeskin(){ $skin = input('post.skin'); if(request()->user['level'] == 2){ if(cookie('admin_skin')){ cookie('admin_skin', null); } - cache('admin_skin', $skin); + config_set('admin_skin', $skin); + Cache::clear(); }else{ cookie('admin_skin', $skin); } diff --git a/app/lib/CheckUtils.php b/app/lib/CheckUtils.php new file mode 100644 index 0000000..e143f8e --- /dev/null +++ b/app/lib/CheckUtils.php @@ -0,0 +1,76 @@ += 400)){ + $status = false; + $errmsg = 'http_code='.$httpcode; + } + $usetime = round(curl_getinfo($ch, CURLINFO_TOTAL_TIME) * 1000); + curl_close($ch); + return ['status'=>$status, 'errmsg'=>$errmsg, 'usetime'=>$usetime]; + } + + public static function tcp($target, $port, $timeout){ + if(!filter_var($target,FILTER_VALIDATE_IP) && checkDomain($target)){ + $target = gethostbyname($target); + if(!$target)return ['status'=>false, 'error'=>'DNS resolve failed', 'usetime'=>0]; + } + $starttime = getMillisecond(); + $fp = @fsockopen($target, $port, $errCode, $errStr, $timeout); + if ($fp) { + $status = true; + fclose($fp); + } else { + $status = false; + } + $endtime = getMillisecond(); + $usetime = $endtime-$starttime; + return ['status'=>$status, 'errmsg'=>$errStr, 'usetime'=>$usetime]; + } + + public static function ping($target){ + if(!function_exists('exec'))return ['status'=>false, 'error'=>'exec函数不可用', 'usetime'=>0]; + if(!filter_var($target,FILTER_VALIDATE_IP) && checkDomain($target)){ + $target = gethostbyname($target); + if(!$target)return ['status'=>false, 'error'=>'DNS resolve failed', 'usetime'=>0]; + } + if(!filter_var($target,FILTER_VALIDATE_IP)){ + return ['status'=>false, 'error'=>'Invalid IP address', 'usetime'=>0]; + } + $timeout = 1; + exec('ping -c 1 -w '.$timeout.' '.$target.'', $output, $return_var); + $usetime = !empty($output[1]) ? round(getSubstr($output[1], 'time=', ' ms')) : 0; + $errmsg = null; + if($return_var !== 0){ + $usetime = $usetime == 0 ? $timeout*1000 : $usetime; + $errmsg = 'ping timeout'; + } + return ['status'=>$return_var===0, 'errmsg'=>$errmsg, 'usetime'=>$usetime]; + } +} \ No newline at end of file diff --git a/app/lib/DnsHelper.php b/app/lib/DnsHelper.php index 34fb727..0fa6047 100644 --- a/app/lib/DnsHelper.php +++ b/app/lib/DnsHelper.php @@ -46,7 +46,7 @@ class DnsHelper 'sk' => 'API密码' ], 'remark' => 0, - 'status' => false, + 'status' => true, 'redirect' => false, 'log' => false, ], @@ -99,4 +99,17 @@ class DnsHelper } return false; } + + public static function getModel2($config) + { + $dnstype = $config['type']; + $class = "\\app\\lib\\dns\\{$dnstype}"; + if(class_exists($class)){ + $config['domain'] = $config['name']; + $config['domainid'] = $config['thirdid']; + $model = new $class($config); + return $model; + } + return false; + } } \ No newline at end of file diff --git a/app/lib/MsgNotice.php b/app/lib/MsgNotice.php new file mode 100644 index 0000000..78b60ee --- /dev/null +++ b/app/lib/MsgNotice.php @@ -0,0 +1,105 @@ +您的域名 '.$task['domain'].''.$task['main_value'].' 记录发生了异常'; + if($task['type'] == 2){ + $mail_content .= ',已自动切换为备用解析记录 '.$task['backup_value'].' '; + }elseif($task['type'] == 1){ + $mail_content .= ',已自动暂停解析'; + }else{ + $mail_content .= ',请及时处理'; + } + if(!empty($result['errmsg'])){ + $mail_content .= '。
异常信息:'.$result['errmsg']; + } + }else{ + $mail_title = 'DNS容灾切换-恢复正常通知'; + $mail_content = '尊敬的系统管理员,您好:
您的域名 '.$task['domain'].''.$task['main_value'].' 记录已恢复正常'; + if($task['type'] == 2){ + $mail_content .= ',已自动切换回当前解析记录'; + }elseif($task['type'] == 1){ + $mail_content .= ',已自动开启解析'; + } + $lasttime = convert_second(time() - $task['switchtime']); + $mail_content .= '。
异常持续时间:'.$lasttime; + } + if(!empty($task['remark'])) $mail_title .= '('.$task['remark'].')'; + if(!empty($task['remark'])) $mail_content .= '
备注:'.$task['remark']; + $mail_content .= '
'.self::$sitename.'
'.date('Y-m-d H:i:s'); + + if(config_get('notice_mail') == 1){ + $mail_name = config_get('mail_recv')?config_get('mail_recv'):config_get('mail_name'); + self::send_mail($mail_name, $mail_title, $mail_content); + } + if(config_get('notice_wxtpl') == 1){ + $mail_content = str_replace(['
', '', ''], ["\n\n", '**', '**'], $mail_content); + self::send_wechat_tplmsg($mail_title, $mail_content); + } + } + + public static function send_mail($to, $sub, $msg){ + $mail_type = config_get('mail_type'); + if($mail_type == 1){ + $mail = new \app\lib\mail\Sendcloud(config_get('mail_apiuser'), config_get('mail_apikey')); + return $mail->send($to, $sub, $msg, config_get('mail_name'), self::$sitename); + }elseif($mail_type == 2){ + $mail = new \app\lib\mail\Aliyun(config_get('mail_apiuser'), config_get('mail_apikey')); + return $mail->send($to, $sub, $msg, config_get('mail_name'), self::$sitename); + }else{ + $mail_name = config_get('mail_name'); + $mail_port = intval(config_get('mail_port')); + $mail_smtp = config_get('mail_smtp'); + $mail_pwd = config_get('mail_pwd'); + if(!$mail_name || !$mail_port || !$mail_smtp || !$mail_pwd)return false; + $mail = new \app\lib\mail\PHPMailer\PHPMailer(true); + try{ + $mail->SMTPDebug = 0; + $mail->CharSet = 'UTF-8'; + $mail->Timeout = 5; + $mail->isSMTP(); + $mail->Host = $mail_smtp; + $mail->SMTPAuth = true; + $mail->Username = $mail_name; + $mail->Password = $mail_pwd; + if($mail_port == 587) $mail->SMTPSecure = 'tls'; + else if($mail_port >= 465) $mail->SMTPSecure = 'ssl'; + else $mail->SMTPAutoTLS = false; + $mail->Port = $mail_port; + $mail->setFrom($mail_name, self::$sitename); + $mail->addAddress($to); + $mail->addReplyTo($mail_name, self::$sitename); + $mail->isHTML(true); + $mail->Subject = $sub; + $mail->Body = $msg; + $mail->send(); + return true; + } catch (\Exception $e) { + return $mail->ErrorInfo; + } + } + } + + public static function send_wechat_tplmsg($title, $content){ + $wechat_apptoken = config_get('wechat_apptoken'); + $wechat_appuid = config_get('wechat_appuid'); + if(!$wechat_apptoken||!$wechat_appuid)return false; + $url = 'https://wxpusher.zjiecode.com/api/send/message'; + $post = ['appToken'=>$wechat_apptoken, 'content'=>$content, 'summary'=>$title, 'contentType'=>3, 'uids'=>[$wechat_appuid]]; + $result = get_curl($url, json_encode($post),0,0,0,0,0,['Content-Type: application/json; charset=UTF-8']); + $arr = json_decode($result, true); + if(isset($arr['success']) && $arr['success']==true){ + return true; + }else{ + return $arr['msg']; + } + } +} \ No newline at end of file diff --git a/app/lib/NewDb.php b/app/lib/NewDb.php new file mode 100644 index 0000000..2a1d6d0 --- /dev/null +++ b/app/lib/NewDb.php @@ -0,0 +1,17 @@ +getConfig('default', 'mysql'); + } + + return $this->createConnection($name); + } +} diff --git a/app/lib/TaskRunner.php b/app/lib/TaskRunner.php new file mode 100644 index 0000000..fd28164 --- /dev/null +++ b/app/lib/TaskRunner.php @@ -0,0 +1,111 @@ +conn){ + $this->conn = NewDb::connect(); + } + return $this->conn; + } + + private function closeDb() + { + if($this->conn){ + $this->conn->close(); + } + } + + public function execute($row) + { + if($row['checktype'] == 2){ + $result = CheckUtils::curl($row['checkurl'], $row['timeout']); + }else if($row['checktype'] == 1){ + $result = CheckUtils::tcp($row['main_value'], $row['tcpport'], $row['timeout']); + }else{ + $result = CheckUtils::ping($row['main_value']); + } + + $action = 0; + if($result['status'] && $row['status']==1){ + if($row['cycle'] <= 1 || $row['errcount'] >= $row['cycle']){ + $this->db()->name('dmtask')->where('id', $row['id'])->update(['status'=>0, 'errcount'=>0, 'switchtime'=>time()]); + $action = 2; + }else{ + $this->db()->name('dmtask')->where('id', $row['id'])->inc('errcount')->update(); + } + }elseif(!$result['status'] && $row['status']==0){ + if($row['cycle'] <= 1 || $row['errcount'] >= $row['cycle']){ + $this->db()->name('dmtask')->where('id', $row['id'])->update(['status'=>1, 'errcount'=>0, 'switchtime'=>time()]); + $action = 1; + }else{ + $this->db()->name('dmtask')->where('id', $row['id'])->inc('errcount')->update(); + } + }elseif($row['errcount'] > 0){ + $this->db()->name('dmtask')->where('id', $row['id'])->update(['errcount'=>0]); + } + + if($action > 0){ + $drow = $this->db()->name('domain')->alias('A')->join('account B','A.aid = B.id')->where('A.id', $row['did'])->field('A.*,B.type,B.ak,B.sk,B.ext')->find(); + if(!$drow){ + echo '域名不存在(ID:'.$row['did'].')'."\n"; + $this->closeDb(); + return; + } + $row['domain'] = $row['rr'] . '.' . $drow['name']; + } + if($action == 1){ + if($row['type'] == 2){ + $dns = DnsHelper::getModel2($drow); + $recordinfo = json_decode($row['recordinfo'], true); + $res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['backup_value']), $row['backup_value'], $recordinfo['Line'], $recordinfo['TTL']); + if(!$res){ + $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '修改解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); + } + }elseif($row['type'] == 1){ + $dns = DnsHelper::getModel2($drow); + $res = $dns->setDomainRecordStatus($row['recordid'], '0'); + if(!$res){ + $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '暂停解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); + } + } + }elseif($action == 2){ + if($row['type'] == 2){ + $dns = DnsHelper::getModel2($drow); + $recordinfo = json_decode($row['recordinfo'], true); + $res = $dns->updateDomainRecord($row['recordid'], $row['rr'], getDnsType($row['main_value']), $row['main_value'], $recordinfo['Line'], $recordinfo['TTL']); + if(!$res){ + $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '修改解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); + } + }elseif($row['type'] == 1){ + $dns = DnsHelper::getModel2($drow); + $res = $dns->setDomainRecordStatus($row['recordid'], '1'); + if(!$res){ + $this->db()->name('log')->insert(['uid' => 0, 'domain' => $drow['name'], 'action' => '启用解析失败', 'data' => $dns->getError(), 'addtime' => date("Y-m-d H:i:s")]); + } + } + }else{ + $this->closeDb(); + return; + } + + $this->db()->name('dmlog')->insert([ + 'taskid' => $row['id'], + 'action' => $action, + 'errmsg' => $result['status'] ? null : $result['errmsg'], + 'date' => date('Y-m-d H:i:s') + ]); + $this->closeDb(); + + MsgNotice::send($action, $row, $result); + } +} \ No newline at end of file diff --git a/app/lib/dns/aliyun.php b/app/lib/dns/aliyun.php index c6d366a..de76963 100644 --- a/app/lib/dns/aliyun.php +++ b/app/lib/dns/aliyun.php @@ -86,7 +86,7 @@ class aliyun implements DnsInterface { //获取子域名解析记录列表 public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){ - $param = ['Action' => 'DescribeSubDomainRecords', 'SubDomain' => $SubDomain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'Type' => $Type, 'Line' => $Line]; + $param = ['Action' => 'DescribeSubDomainRecords', 'SubDomain' => $SubDomain . '.' . $this->domain, 'PageNumber' => $PageNumber, 'PageSize' => $PageSize, 'Type' => $Type, 'Line' => $Line]; $data = $this->request($param, true); if($data){ $list = []; diff --git a/app/lib/dns/baidu.php b/app/lib/dns/baidu.php index 08bf3c2..d3d34f0 100644 --- a/app/lib/dns/baidu.php +++ b/app/lib/dns/baidu.php @@ -80,11 +80,8 @@ class baidu implements DnsInterface { //获取子域名解析记录列表 public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){ - $domain_arr = explode('.', $SubDomain); - $domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1]; - $subdomain = rtrim(str_replace($domain,'',$SubDomain),'.'); - if($subdomain == '')$subdomain='@'; - return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line); + if($SubDomain == '')$SubDomain='@'; + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line); } //获取解析记录详细信息 diff --git a/app/lib/dns/cloudflare.php b/app/lib/dns/cloudflare.php index b9dc942..1cf2895 100644 --- a/app/lib/dns/cloudflare.php +++ b/app/lib/dns/cloudflare.php @@ -57,10 +57,11 @@ class cloudflare implements DnsInterface { if($data){ $list = []; foreach($data['result'] as $row){ + $name = $row['zone_name'] == $row['name'] ? '@' : str_replace('.'.$row['zone_name'], '', $row['name']); $list[] = [ 'RecordId' => $row['id'], 'Domain' => $row['zone_name'], - 'Name' => str_replace('.'.$row['zone_name'], '', $row['name']), + 'Name' => $name, 'Type' => $row['type'], 'Value' => $row['content'], 'Line' => $row['proxied'] ? '1' : '0', @@ -79,17 +80,16 @@ class cloudflare implements DnsInterface { //获取子域名解析记录列表 public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){ - $domain_arr = explode('.', $SubDomain); - $domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1]; - $subdomain = rtrim(str_replace($domain,'',$SubDomain),'.'); - if($subdomain == '')$subdomain='@'; - return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line); + if($SubDomain == '@')$SubDomain=$this->domain; + else $SubDomain .= '.'.$this->domain; + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line); } //获取解析记录详细信息 public function getDomainRecordInfo($RecordId){ $data = $this->send_reuqest('GET', '/zones/'.$this->domainid.'/dns_records/'.$RecordId); if($data){ + $name = $data['result']['zone_name'] == $data['result']['name'] ? '@' : str_replace('.'.$data['result']['zone_name'], '', $data['result']['name']); return [ 'RecordId' => $data['result']['id'], 'Domain' => $data['result']['zone_name'], diff --git a/app/lib/dns/dnsla.php b/app/lib/dns/dnsla.php index dfbf155..41025da 100644 --- a/app/lib/dns/dnsla.php +++ b/app/lib/dns/dnsla.php @@ -89,11 +89,8 @@ class dnsla implements DnsInterface { //获取子域名解析记录列表 public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){ - $domain_arr = explode('.', $SubDomain); - $domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1]; - $subdomain = rtrim(str_replace($domain,'',$SubDomain),'.'); - if($subdomain == '')$subdomain='@'; - return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line); + if($SubDomain == '')$SubDomain='@'; + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line); } //获取解析记录详细信息 diff --git a/app/lib/dns/dnspod.php b/app/lib/dns/dnspod.php index 28129a7..0049b3e 100644 --- a/app/lib/dns/dnspod.php +++ b/app/lib/dns/dnspod.php @@ -90,11 +90,8 @@ class dnspod implements DnsInterface { //获取子域名解析记录列表 public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){ - $domain_arr = explode('.', $SubDomain); - $domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1]; - $subdomain = rtrim(str_replace($domain,'',$SubDomain),'.'); - if($subdomain == '')$subdomain='@'; - return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line); + if($SubDomain == '')$SubDomain='@'; + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line); } //获取解析记录详细信息 diff --git a/app/lib/dns/huawei.php b/app/lib/dns/huawei.php index 9565d3f..57c2789 100644 --- a/app/lib/dns/huawei.php +++ b/app/lib/dns/huawei.php @@ -7,8 +7,6 @@ class huawei implements DnsInterface { private $AccessKeyId; private $SecretAccessKey; private $endpoint = "dns.myhuaweicloud.com"; - private $service = "dnspod"; - private $version = "2021-03-23"; private $error; private $domain; private $domainid; @@ -55,7 +53,7 @@ class huawei implements DnsInterface { $offset = ($PageNumber-1)*$PageSize; $query = ['type' => $Type, 'line_id' => $Line, 'name' => $KeyWord, 'status' => $Status, 'offset' => $offset, 'limit' => $PageSize]; if(!isNullOrEmpty(($SubDomain))){ - $param['name'] = $SubDomain; + $query['name'] = $SubDomain; } $data = $this->send_reuqest('GET', '/v2.1/zones/'.$this->domainid.'/recordsets', $query); if($data){ @@ -84,11 +82,8 @@ class huawei implements DnsInterface { //获取子域名解析记录列表 public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){ - $domain_arr = explode('.', $SubDomain); - $domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1]; - $subdomain = rtrim(str_replace($domain,'',$SubDomain),'.'); - if($subdomain == '')$subdomain='@'; - return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line); + if($SubDomain == '')$SubDomain='@'; + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line); } //获取解析记录详细信息 diff --git a/app/lib/dns/west.php b/app/lib/dns/west.php index 5b8c898..c0d5857 100644 --- a/app/lib/dns/west.php +++ b/app/lib/dns/west.php @@ -3,6 +3,9 @@ namespace app\lib\dns; use app\lib\DnsInterface; +/** + * @see http://apipost.west.cn/ + */ class west implements DnsInterface { private $username; private $api_password; @@ -30,8 +33,8 @@ class west implements DnsInterface { //获取域名列表 public function getDomainList($KeyWord=null, $PageNumber=1, $PageSize=20){ - $param = ['page' => $PageNumber, 'limit' => $PageSize, 'domain' => $KeyWord]; - $data = $this->execute('/domain/?act=getdomains', $param); + $param = ['act' => 'getdomains', 'page' => $PageNumber, 'limit' => $PageSize, 'domain' => $KeyWord]; + $data = $this->execute('/domain/', $param); if($data){ $list = []; foreach($data['items'] as $row){ @@ -48,23 +51,23 @@ class west implements DnsInterface { //获取解析记录列表 public function getDomainRecords($PageNumber=1, $PageSize=20, $KeyWord = null, $SubDomain = null, $Type = null, $Line = null, $Status = null){ - $param = ['act' => 'dnsrec.list', 'domain' => $this->domain, 'record_type' => $Type, 'record_line' => $Line, 'hostname' => $KeyWord, 'pageno' => $PageNumber, 'limit' => $PageSize]; + $param = ['act' => 'getdnsrecord', 'domain' => $this->domain, 'type' => $Type, 'line' => $Line, 'host' => $KeyWord, 'pageno' => $PageNumber, 'limit' => $PageSize]; if(!isNullOrEmpty(($SubDomain))){ - $param['hostname'] = $SubDomain; + $param['host'] = $SubDomain; } - $data = $this->execute2('/domain/dns/', $param); + $data = $this->execute('/domain/', $param); if($data){ $list = []; foreach($data['items'] as $row){ $list[] = [ - 'RecordId' => $row['record_id'], + 'RecordId' => $row['id'], 'Domain' => $this->domain, - 'Name' => $row['hostname'], - 'Type' => $row['record_type'], - 'Value' => $row['record_value'], - 'Line' => $row['record_line'], - 'TTL' => $row['record_ttl'], - 'MX' => $row['record_mx'], + 'Name' => $row['item'], + 'Type' => $row['type'], + 'Value' => $row['value'], + 'Line' => $row['line'], + 'TTL' => $row['ttl'], + 'MX' => $row['level'], 'Status' => $row['pause'] == 1 ? '0' : '1', 'Weight' => null, 'Remark' => null, @@ -78,11 +81,8 @@ class west implements DnsInterface { //获取子域名解析记录列表 public function getSubDomainRecords($SubDomain, $PageNumber=1, $PageSize=20, $Type = null, $Line = null){ - $domain_arr = explode('.', $SubDomain); - $domain = $domain_arr[count($domain_arr)-2].'.'.$domain_arr[count($domain_arr)-1]; - $subdomain = rtrim(str_replace($domain,'',$SubDomain),'.'); - if($subdomain == '')$subdomain='@'; - return $this->getDomainRecords($PageNumber, $PageSize, null, $subdomain, $Type, $Line); + if($SubDomain == '')$SubDomain='@'; + return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, $Type, $Line); } //获取解析记录详细信息 @@ -92,15 +92,15 @@ class west implements DnsInterface { //添加解析记录 public function addDomainRecord($Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Remark = null){ - $param = ['act' => 'dnsrec.add', 'domain' => $this->domain, 'hostname' => $Name, 'record_type' => $this->convertType($Type), 'record_value' => $Value, 'record_level' => $MX, 'record_ttl' => intval($TTL), 'record_line' => $Line]; - $data = $this->execute2('/domain/dns/', $param); - return is_array($data) ? $data['record_id'] : false; + $param = ['act' => 'adddnsrecord', 'domain' => $this->domain, 'host' => $Name, 'type' => $this->convertType($Type), 'value' => $Value, 'level' => $MX, 'ttl' => intval($TTL), 'line' => $Line]; + $data = $this->execute('/domain/', $param); + return is_array($data) ? $data['id'] : false; } //修改解析记录 public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = '0', $TTL = 600, $MX = 1, $Remark = null){ - $param = ['act' => 'dnsrec.modify', 'domain' => $this->domain, 'record_id' => $RecordId, 'record_type' => $this->convertType($Type), 'record_value' => $Value, 'record_level' => $MX, 'record_ttl' => intval($TTL), 'record_line' => $Line]; - $data = $this->execute2('/domain/dns/', $param); + $param = ['act' => 'moddnsrecord', 'domain' => $this->domain, 'id' => $RecordId, 'type' => $this->convertType($Type), 'value' => $Value, 'level' => $MX, 'ttl' => intval($TTL), 'line' => $Line]; + $data = $this->execute('/domain/', $param); return is_array($data); } @@ -111,14 +111,16 @@ class west implements DnsInterface { //删除解析记录 public function deleteDomainRecord($RecordId){ - $param = ['act' => 'dnsrec.remove', 'domain' => $this->domain, 'record_id' => $RecordId]; - $data = $this->execute2('/domain/dns/', $param); + $param = ['act' => 'deldnsrecord', 'domain' => $this->domain, 'id' => $RecordId]; + $data = $this->execute('/domain/', $param); return is_array($data); } //设置解析记录状态 public function setDomainRecordStatus($RecordId, $Status){ - return false; + $param = ['act' => 'pause', 'domain' => $this->domain, 'id' => $RecordId, 'val' => $Status == '1' ? '0' : '1']; + $data = $this->execute('/domain/', $param); + return $data !== false; } //获取解析记录操作日志 @@ -162,26 +164,7 @@ class west implements DnsInterface { $arr=json_decode($response,true); if($arr){ if($arr['result'] == 200){ - return $arr['data']; - }else{ - $this->setError($arr['msg']); - return false; - } - }else{ - $this->setError('返回数据解析失败'); - return false; - } - } - - private function execute2($path, $params){ - $params['username'] = $this->username; - $params['apikey'] = md5($this->api_password); - $response = $this->curl($path, $params); - $response = mb_convert_encoding($response, 'UTF-8', 'GBK'); - $arr=json_decode($response,true); - if($arr){ - if($arr['code'] == 200){ - return $arr['body']; + return isset($arr['data']) ? $arr['data'] : []; }else{ $this->setError($arr['msg']); return false; diff --git a/app/lib/mail/Aliyun.php b/app/lib/mail/Aliyun.php new file mode 100644 index 0000000..82225f8 --- /dev/null +++ b/app/lib/mail/Aliyun.php @@ -0,0 +1,72 @@ +AccessKeyId = $AccessKeyId; + $this->AccessKeySecret = $AccessKeySecret; + } + private function aliyunSignature($parameters, $accessKeySecret, $method) + { + ksort($parameters); + $canonicalizedQueryString = ''; + foreach ($parameters as $key => $value) { + if($value === null) continue; + $canonicalizedQueryString .= '&' . $this->percentEncode($key) . '=' . $this->percentEncode($value); + } + $stringToSign = $method . '&%2F&' . $this->percentencode(substr($canonicalizedQueryString, 1)); + $signature = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&", true)); + + return $signature; + } + private function percentEncode($str) + { + $search = ['+', '*', '%7E']; + $replace = ['%20', '%2A', '~']; + return str_replace($search, $replace, urlencode($str)); + } + public function send($to, $sub, $msg, $from, $from_name) + { + if (empty($this->AccessKeyId) || empty($this->AccessKeySecret)) return false; + $url = 'https://dm.aliyuncs.com/'; + $data = array( + 'Action' => 'SingleSendMail', + 'AccountName' => $from, + 'ReplyToAddress' => 'false', + 'AddressType' => 1, + 'ToAddress' => $to, + 'FromAlias' => $from_name, + 'Subject' => $sub, + 'HtmlBody' => $msg, + 'Format' => 'JSON', + 'Version' => '2015-11-23', + 'AccessKeyId' => $this->AccessKeyId, + 'SignatureMethod' => 'HMAC-SHA1', + 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), + 'SignatureVersion' => '1.0', + 'SignatureNonce' => random(8) + ); + $data['Signature'] = $this->aliyunSignature($data, $this->AccessKeySecret, 'POST'); + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + $json = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + $arr = json_decode($json, true); + if ($httpCode == 200) { + return true; + } else { + return $arr['Message']; + } + } +} \ No newline at end of file diff --git a/app/lib/mail/PHPMailer/Exception.php b/app/lib/mail/PHPMailer/Exception.php new file mode 100644 index 0000000..f784b9f --- /dev/null +++ b/app/lib/mail/PHPMailer/Exception.php @@ -0,0 +1,2 @@ +' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "
\n"; } } \ No newline at end of file diff --git a/app/lib/mail/PHPMailer/PHPMailer.php b/app/lib/mail/PHPMailer/PHPMailer.php new file mode 100644 index 0000000..1a4e34f --- /dev/null +++ b/app/lib/mail/PHPMailer/PHPMailer.php @@ -0,0 +1,2 @@ +exceptions = (bool) $exceptions; } $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html'); } public function __destruct() { $this->smtpClose(); } private function mailPassthru($to, $subject, $body, $header, $params) { if ((int)ini_get('mbstring.func_overload') & 1) { $subject = $this->secureHeader($subject); } else { $subject = $this->encodeHeader($this->secureHeader($subject)); } $this->edebug('Sending with mail()'); $this->edebug('Sendmail path: ' . ini_get('sendmail_path')); $this->edebug("Envelope sender: {$this->Sender}"); $this->edebug("To: {$to}"); $this->edebug("Subject: {$subject}"); $this->edebug("Headers: {$header}"); if (!$this->UseSendmailOptions || null === $params) { $result = @mail($to, $subject, $body, $header); } else { $this->edebug("Additional params: {$params}"); $result = @mail($to, $subject, $body, $header, $params); } $this->edebug('Result: ' . ($result ? 'true' : 'false')); return $result; } protected function edebug($str) { if ($this->SMTPDebug <= 0) { return; } if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { $this->Debugoutput->debug(rtrim($str, "\r\n")); return; } 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': error_log($str); break; case 'html': echo htmlentities(preg_replace('/[\r\n]+/', '', $str), ENT_QUOTES, 'UTF-8'), "
\n"; break; case 'echo': default: $str = preg_replace('/\r\n|\r/m', "\n", $str); echo gmdate('Y-m-d H:i:s'), "\t", trim(str_replace("\n", "\n \t ", trim($str))), "\n"; } } public function isHTML($isHtml = true) { if ($isHtml) { $this->ContentType = static::CONTENT_TYPE_TEXT_HTML; } else { $this->ContentType = static::CONTENT_TYPE_PLAINTEXT; } } public function isSMTP() { $this->Mailer = 'smtp'; } public function isMail() { $this->Mailer = 'mail'; } 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'; } 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'; } public function addAddress($address, $name = '') { return $this->addOrEnqueueAnAddress('to', $address, $name); } public function addCC($address, $name = '') { return $this->addOrEnqueueAnAddress('cc', $address, $name); } public function addBCC($address, $name = '') { return $this->addOrEnqueueAnAddress('bcc', $address, $name); } public function addReplyTo($address, $name = '') { return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); } protected function addOrEnqueueAnAddress($kind, $address, $name) { $pos = false; if ($address !== null) { $address = trim($address); $pos = strrpos($address, '@'); } if (false === $pos) { $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 ($name !== null && is_string($name)) { $name = trim(preg_replace('/[\r\n]+/', '', $name)); } else { $name = ''; } $params = [$kind, $address, $name]; 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; } return call_user_func_array([$this, 'addAnAddress'], $params); } public function setBoundaries() { $this->uniqueid = $this->generateId(); $this->boundary[1] = 'b1=_' . $this->uniqueid; $this->boundary[2] = 'b2=_' . $this->uniqueid; $this->boundary[3] = 'b3=_' . $this->uniqueid; } 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; } public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591) { $addresses = []; if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { $list = imap_rfc822_parse_adrlist($addrstr, ''); imap_errors(); foreach ($list as $address) { if ('.SYNTAX-ERROR.' !== $address->host && static::validateAddress($address->mailbox . '@' . $address->host)) { if (property_exists($address, 'personal') && defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $address->personal)) { $origCharset = mb_internal_encoding(); mb_internal_encoding($charset); $address->personal = str_replace('_', '=20', $address->personal); $address->personal = mb_decode_mimeheader($address->personal); mb_internal_encoding($origCharset); } $addresses[] = ['name' => (property_exists($address, 'personal') ? $address->personal : ''), 'address' => $address->mailbox . '@' . $address->host,]; } } } else { $list = explode(',', $addrstr); foreach ($list as $address) { $address = trim($address); if (strpos($address, '<') === false) { if (static::validateAddress($address)) { $addresses[] = ['name' => '', 'address' => $address,]; } } else { list($name, $email) = explode('<', $address); $email = trim(str_replace('>', '', $email)); $name = trim($name); if (static::validateAddress($email)) { if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { $origCharset = mb_internal_encoding(); mb_internal_encoding($charset); $name = str_replace('_', '=20', $name); $name = mb_decode_mimeheader($name); mb_internal_encoding($origCharset); } $addresses[] = ['name' => trim($name, '\'" '), 'address' => $email,]; } } } } return $addresses; } public function setFrom($address, $name = '', $auto = true) { $address = trim((string)$address); $name = trim(preg_replace('/[\r\n]+/', '', $name)); $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; } public function getLastMessageID() { return $this->lastMessageID; } public static function validateAddress($address, $patternselect = null) { if (null === $patternselect) { $patternselect = static::$validator; } if (is_callable($patternselect) && !is_string($patternselect)) { return call_user_func($patternselect, $address); } if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) { return false; } switch ($patternselect) { case 'pcre': case 'pcre8': 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': 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 filter_var($address, FILTER_VALIDATE_EMAIL) !== false; } } public static function idnSupported() { return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding'); } public function punyencodeAddress($address) { $pos = strrpos($address, '@'); if (!empty($this->CharSet) && false !== $pos && static::idnSupported()) { $domain = substr($address, ++$pos); if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) { $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet); $errorcode = 0; if (defined('INTL_IDNA_VARIANT_UTS46')) { $punycode = idn_to_ascii($domain, \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46); } elseif (defined('INTL_IDNA_VARIANT_2003')) { $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003); } else { $punycode = idn_to_ascii($domain, $errorcode); } if (false !== $punycode) { return substr($address, 0, $pos) . $punycode; } } } return $address; } 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; } } public function preSend() { if ('smtp' === $this->Mailer || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))) { static::setLE(self::CRLF); } else { static::setLE(PHP_EOL); } 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($this->lang('buggy_php'), E_USER_WARNING); } try { $this->error_count = 0; $this->mailHeader = ''; 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); } foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) { if ($this->{$address_kind} === null) { $this->{$address_kind} = ''; continue; } $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; } } if ($this->alternativeExists()) { $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE; } $this->setMessageType(); if (!$this->AllowEmpty && empty($this->Body)) { throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); } $this->Subject = trim($this->Subject); $this->MIMEHeader = ''; $this->MIMEBody = $this->createBody(); $tempheaders = $this->MIMEHeader; $this->MIMEHeader = $this->createHeader(); $this->MIMEHeader .= $tempheaders; 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))); } 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 = static::stripTrailingWSP($this->MIMEHeader) . static::$LE . static::normalizeBreaks($header_dkim) . static::$LE; } return true; } catch (Exception $exc) { $this->setError($exc->getMessage()); if ($this->exceptions) { throw $exc; } return false; } } public function postSend() { try { 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->Mailer === 'smtp' && $this->SMTPKeepAlive == true && $this->smtp->connected()) { $this->smtp->reset(); } if ($this->exceptions) { throw $exc; } } return false; } protected function sendmailSend($header, $body) { if ($this->Mailer === 'qmail') { $this->edebug('Sending with qmail'); } else { $this->edebug('Sending with sendmail'); } $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; $sendmail_from_value = ini_get('sendmail_from'); if (empty($this->Sender) && !empty($sendmail_from_value)) { $this->Sender = ini_get('sendmail_from'); } if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) { if ($this->Mailer === 'qmail') { $sendmailFmt = '%s -f%s'; } else { $sendmailFmt = '%s -oi -f%s -t'; } } else { $sendmailFmt = '%s -oi -t'; } $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); $this->edebug('Sendmail path: ' . $this->Sendmail); $this->edebug('Sendmail command: ' . $sendmail); $this->edebug('Envelope sender: ' . $this->Sender); $this->edebug("Headers: {$header}"); 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); } $this->edebug("To: {$toAddr}"); fwrite($mail, 'To: ' . $toAddr . "\n"); fwrite($mail, $header); fwrite($mail, $body); $result = pclose($mail); $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); $this->doCallback(($result === 0), [[$addrinfo['address'], $addrinfo['name']]], $this->cc, $this->bcc, $this->Subject, $body, $this->From, []); $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); 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, []); $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } return true; } protected static function isShellSafe($string) { if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) { return false; } if (escapeshellcmd($string) !== $string || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])) { return false; } $length = strlen($string); for ($i = 0; $i < $length; ++$i) { $c = $string[$i]; if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { return false; } } return true; } protected static function isPermittedPath($path) { return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path); } protected static function fileIsAccessible($path) { if (!static::isPermittedPath($path)) { return false; } $readable = is_file($path); if (strpos($path, '\\\\') !== 0) { $readable = $readable && is_readable($path); } return $readable; } protected function mailSend($header, $body) { $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; $toArr = []; foreach ($this->to as $toaddr) { $toArr[] = $this->addrFormat($toaddr); } $to = trim(implode(', ', $toArr)); if ($to === '') { $to = 'undisclosed-recipients:;'; } $params = null; $sendmail_from_value = ini_get('sendmail_from'); if (empty($this->Sender) && !empty($sendmail_from_value)) { $this->Sender = ini_get('sendmail_from'); } if (!empty($this->Sender) && static::validateAddress($this->Sender)) { if (self::isShellSafe($this->Sender)) { $params = sprintf('-f%s', $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); $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); $this->doCallback($result, [[$addrinfo['address'], $addrinfo['name']]], $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; } public function getSMTPInstance() { if (!is_object($this->smtp)) { $this->smtp = new SMTP(); } return $this->smtp; } public function setSMTPInstance(SMTP $smtp) { $this->smtp = $smtp; return $this->smtp; } public function setSMTPXclientAttribute($name, $value) { if (!in_array($name, SMTP::$xclient_allowed_attributes)) { return false; } if (isset($this->SMTPXClient[$name]) && $value === null) { unset($this->SMTPXClient[$name]); } elseif ($value !== null) { $this->SMTPXClient[$name] = $value; } return true; } public function getSMTPXclientAttributes() { return $this->SMTPXClient; } protected function smtpSend($header, $body) { $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; $bad_rcpt = []; if (!$this->smtpConnect($this->SMTPOptions)) { throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); } if ('' === $this->Sender) { $smtp_from = $this->From; } else { $smtp_from = $this->Sender; } if (count($this->SMTPXClient)) { $this->smtp->xclient($this->SMTPXClient); } 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 = []; 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], 'name' => $to[1]]; } } 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'], $cb['name']]], [], [], $this->Subject, $body, $this->From, ['smtp_transaction_id' => $smtp_transaction_id]); } 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; } public function smtpConnect($options = null) { if (null === $this->smtp) { $this->smtp = $this->getSMTPInstance(); } if (null === $options) { $options = $this->SMTPOptions; } 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); if ($this->Host === null) { $this->Host = 'localhost'; } $hosts = explode(';', $this->Host); $lastexception = null; foreach ($hosts as $hostentry) { $hostinfo = []; if (!preg_match('/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/', trim($hostentry), $hostinfo)) { $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry)); continue; } if (!static::isValidHost($hostinfo[2])) { $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]); continue; } $prefix = ''; $secure = $this->SMTPSecure; $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure); if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) { $prefix = 'ssl://'; $tls = false; $secure = static::ENCRYPTION_SMTPS; } elseif ('tls' === $hostinfo[1]) { $tls = true; $secure = static::ENCRYPTION_STARTTLS; } $sslext = defined('OPENSSL_ALGO_SHA256'); if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) { if (!$sslext) { throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); } } $host = $hostinfo[2]; $port = $this->Port; if (array_key_exists(3, $hostinfo) && is_numeric($hostinfo[3]) && $hostinfo[3] > 0 && $hostinfo[3] < 65536) { $port = (int) $hostinfo[3]; } 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); if ($this->SMTPAutoTLS && $this->Host !== 'localhost' && $sslext && $secure !== 'ssl' && $this->smtp->getServerExt('STARTTLS')) { $tls = true; } if ($tls) { if (!$this->smtp->startTLS()) { $message = $this->getSmtpErrorMessage('connect_host'); throw new Exception($message); } $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()); $this->smtp->quit(); } } } $this->smtp->close(); if ($this->exceptions && null !== $lastexception) { throw $lastexception; } if ($this->exceptions) { $message = $this->getSmtpErrorMessage('connect_host'); throw new Exception($message); } return false; } public function smtpClose() { if ((null !== $this->smtp) && $this->smtp->connected()) { $this->smtp->quit(); $this->smtp->close(); } } public function setLanguage($langcode = 'en', $lang_path = '') { $renamed_langcodes = ['br' => 'pt_br', 'cz' => 'cs', 'dk' => 'da', 'no' => 'nb', 'se' => 'sv', 'rs' => 'sr', 'tg' => 'tl', 'am' => 'hy',]; if (array_key_exists($langcode, $renamed_langcodes)) { $langcode = $renamed_langcodes[$langcode]; } $PHPMAILER_LANG = ['authenticate' => 'SMTP Error: Could not authenticate.', 'buggy_php' => '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+.', '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: ', 'extension_missing' => 'Extension missing: ', '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: ', 'invalid_header' => 'Invalid header name or value', 'invalid_hostentry' => 'Invalid hostentry: ', 'invalid_host' => 'Invalid host: ', '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_code' => 'SMTP code: ', 'smtp_code_ex' => 'Additional SMTP info: ', 'smtp_connect_failed' => 'SMTP connect() failed.', 'smtp_detail' => 'Detail: ', 'smtp_error' => 'SMTP server error: ', 'variable_set' => 'Cannot set or reset variable: ',]; $PHPMAILER_LANG['authenticate'] = 'SMTP登录失败:邮箱账号或密码错误。'; $PHPMAILER_LANG['buggy_php'] = '您的 PHP 版本存在漏洞,可能会导致消息损坏。为修复此问题,请切换到使用 SMTP 发送,在您的 php.ini 中禁用 mail.add_x_header 选项。切换到 MacOS 或 Linux,或将您的 PHP 升级到 7.0.17+ 或 7.1.3+ 版本。'; $PHPMAILER_LANG['connect_host'] = '无法连接到SMTP服务器。'; $PHPMAILER_LANG['data_not_accepted'] = '数据不被接受。'; $PHPMAILER_LANG['empty_message'] = '邮件正文为空。'; $PHPMAILER_LANG['encoding'] = '未知编码:'; $PHPMAILER_LANG['execute'] = '无法执行:'; $PHPMAILER_LANG['extension_missing'] = '缺少扩展名:'; $PHPMAILER_LANG['file_access'] = '无法访问文件:'; $PHPMAILER_LANG['file_open'] = '文件错误:无法打开文件:'; $PHPMAILER_LANG['from_failed'] = '发送地址错误:'; $PHPMAILER_LANG['instantiate'] = '未知函数调用。'; $PHPMAILER_LANG['invalid_address'] = '发送失败,电子邮箱地址是无效的:'; $PHPMAILER_LANG['mailer_not_supported'] = '发信客户端不被支持。'; $PHPMAILER_LANG['provide_address'] = '必须提供至少一个收件人地址。'; $PHPMAILER_LANG['recipients_failed'] = '收件人地址错误:'; $PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP服务器连接失败:SMTP服务器地址或端口错误。'; $PHPMAILER_LANG['smtp_error'] = 'SMTP服务器出错:'; $PHPMAILER_LANG['variable_set'] = '无法设置或重置变量:'; $PHPMAILER_LANG['invalid_header'] = '无效的标题名称或值'; $PHPMAILER_LANG['invalid_hostentry'] = '无效的hostentry: '; $PHPMAILER_LANG['invalid_host'] = '无效的主机:'; $PHPMAILER_LANG['signing'] = '签名错误:'; $PHPMAILER_LANG['smtp_code'] = 'SMTP代码: '; $PHPMAILER_LANG['smtp_code_ex'] = '附加SMTP信息: '; $PHPMAILER_LANG['smtp_detail'] = '详情:'; if (empty($lang_path)) { $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; } $foundlang = true; $langcode = strtolower($langcode); if (!preg_match('/^(?P[a-z]{2})(?P + +{/block} \ No newline at end of file diff --git a/app/view/dmonitor/overview.html b/app/view/dmonitor/overview.html new file mode 100644 index 0000000..cc774ef --- /dev/null +++ b/app/view/dmonitor/overview.html @@ -0,0 +1,155 @@ +{extend name="common/layout" /} +{block name="title"}容灾切换运行概览{/block} +{block name="main"} + + +
+
+
+ + +
+ 运行状态 + {$info.run_state==1?'正在运行':'已停止'} +
+ +
+ +
+ +
+
+ + +
+ 今日运行次数 + {$info.run_count} +
+ +
+ +
+ + + +
+ +
+
+ + +
+ 24H告警次数 + {$info.fail_count} +
+ +
+ +
+ +
+
+ + +
+ 24H切换次数 + {$info.switch_count} +
+ +
+ +
+ +
+
+
+
+

运行概览

+
+
  • 上次运行时间: {$info.run_time}
  • +
  • 当前时间: {:date('Y-m-d H:i:s')}
  • +
  • Swoole组件: {$info.swoole|raw}
  • + {if $info.run_error}
  • 上次运行错误信息: {$info.run_error}
  • {/if} + +
    +
    +
    +
    +
    +

    操作说明

    +
    +

    1、php需要安装swoole组件

    +

    2、在命令行执行以下命令启动进程:

    +

    cd {:app()->getRootPath()} && php think dmtask

    +

    3、也可以使用进程守护管理器,添加守护进程,运行目录:{:app()->getRootPath()},启动命令:php think dmtask

    +
    +
    +
    +
    +{/block} +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/app/view/dmonitor/task.html b/app/view/dmonitor/task.html new file mode 100644 index 0000000..311b637 --- /dev/null +++ b/app/view/dmonitor/task.html @@ -0,0 +1,169 @@ +{extend name="common/layout" /} +{block name="title"}容灾切换策略{/block} +{block name="main"} + +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + +
    + + 刷新 + 添加 +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + +{/block} \ No newline at end of file diff --git a/app/view/dmonitor/taskform.html b/app/view/dmonitor/taskform.html new file mode 100644 index 0000000..452ac06 --- /dev/null +++ b/app/view/dmonitor/taskform.html @@ -0,0 +1,242 @@ +{extend name="common/layout" /} +{block name="title"}容灾切换策略{/block} +{block name="main"} + +
    +
    +
    +

    返回{if $action=='edit'}编辑{else}添加{/if}容灾切换策略

    +
    +
    +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + +{/block} \ No newline at end of file diff --git a/app/view/dmonitor/taskinfo.html b/app/view/dmonitor/taskinfo.html new file mode 100644 index 0000000..d4a170e --- /dev/null +++ b/app/view/dmonitor/taskinfo.html @@ -0,0 +1,73 @@ +{extend name="common/layout" /} +{block name="title"}切换记录{/block} +{block name="main"} + +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    + + 刷新 +   24H告警次数:{$info.fail_count}  切换次数:{$info.switch_count} +
    + + +
    +
    +
    +
    +
    +{/block} +{block name="script"} + + + + + +{/block} \ No newline at end of file diff --git a/composer.json b/composer.json index c645bc1..c24415b 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ }, "require-dev": { "symfony/var-dumper": "^4.2", - "topthink/think-trace":"^1.0" + "topthink/think-trace":"^1.0", + "swoole/ide-helper": "^5.1" }, "autoload": { "psr-4": { diff --git a/config/app.php b/config/app.php index 8db0724..ce31425 100644 --- a/config/app.php +++ b/config/app.php @@ -30,5 +30,7 @@ return [ // 显示错误信息 'show_error_msg' => false, - 'version' => '1001', + 'version' => '1003', + + 'dbversion' => '1003' ]; diff --git a/config/console.php b/config/console.php index a818a98..f260599 100644 --- a/config/console.php +++ b/config/console.php @@ -5,5 +5,6 @@ return [ // 指令定义 'commands' => [ + 'dmtask' => 'app\command\Dmtask', ], ]; diff --git a/config/database.php b/config/database.php index 0cf1346..a7e4814 100644 --- a/config/database.php +++ b/config/database.php @@ -51,7 +51,7 @@ return [ // 是否严格检查字段是否存在 'fields_strict' => true, // 是否需要断线重连 - 'break_reconnect' => false, + 'break_reconnect' => true, // 监听SQL 'trigger_sql' => env('app_debug', true), // 开启字段缓存 diff --git a/public/index.php b/public/index.php index e3c0fe9..6fd6033 100644 --- a/public/index.php +++ b/public/index.php @@ -12,6 +12,10 @@ // [ 应用入口文件 ] namespace think; +if (version_compare(PHP_VERSION, '7.4.0', '<')) { + die('require PHP >= 7.4 !'); +} + require __DIR__ . '/../vendor/autoload.php'; // 执行HTTP应用并响应 diff --git a/route/app.php b/route/app.php index b32b677..2b32084 100644 --- a/route/app.php +++ b/route/app.php @@ -23,6 +23,7 @@ Route::any('/login', 'auth/login')->middleware(\think\middleware\SessionInit::cl ->middleware(\app\middleware\ViewOutput::class); Route::get('/logout', 'auth/logout'); Route::any('/quicklogin', 'auth/quicklogin'); +Route::any('/dmtask/status', 'dmonitor/status'); Route::group(function () { Route::any('/', 'index/index'); @@ -54,8 +55,19 @@ Route::group(function () { Route::post('/record/remark/:id', 'domain/record_remark'); Route::post('/record/batch/:id', 'domain/record_batch'); Route::any('/record/log/:id', 'domain/record_log'); + Route::post('/record/list', 'domain/record_list'); Route::get('/record/:id', 'domain/record'); + Route::get('/dmonitor/overview', 'dmonitor/overview'); + Route::post('/dmonitor/task/data', 'dmonitor/task_data'); + Route::post('/dmonitor/task/log/data/:id', 'dmonitor/tasklog_data'); + Route::get('/dmonitor/task/info/:id', 'dmonitor/taskinfo'); + Route::any('/dmonitor/task/:action', 'dmonitor/taskform'); + Route::get('/dmonitor/task', 'dmonitor/task'); + Route::any('/dmonitor/noticeset', 'dmonitor/noticeset'); + Route::get('/dmonitor/mailtest', 'dmonitor/mailtest'); + Route::post('/dmonitor/clean', 'dmonitor/clean'); + })->middleware(\app\middleware\CheckLogin::class) ->middleware(\app\middleware\ViewOutput::class);