mirror of
https://github.com/netcccyun/dnsmgr.git
synced 2026-05-09 15:06:28 +02:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
532cecc3bf | ||
|
|
91864aa6be | ||
|
|
9403875044 | ||
|
|
5d53d46659 | ||
|
|
c73f9cd536 | ||
|
|
97dfc1f12f | ||
|
|
a5ec8a3ff6 | ||
|
|
12bdb6cb67 | ||
|
|
a99e3b8642 | ||
|
|
a1cfd470d9 | ||
|
|
945d91386c | ||
|
|
668e2b4ceb | ||
|
|
75a8aa97b8 | ||
|
|
29bcd293ef | ||
|
|
b267d3df86 | ||
|
|
50edcd6dac | ||
|
|
04980fcdd3 | ||
|
|
07a0f54bc1 | ||
|
|
db418c7a11 | ||
|
|
8e4848c14c | ||
|
|
8cbc1f9a18 | ||
|
|
ccda489e81 | ||
|
|
45af1ad464 | ||
|
|
7e49a40057 |
11
.github/docker/Dockerfile
vendored
11
.github/docker/Dockerfile
vendored
@@ -52,13 +52,16 @@ COPY config/php.ini ${PHP_INI_DIR}/conf.d/custom.ini
|
||||
# Configure supervisord
|
||||
COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# CACHE_BUST 须写进每条相关 RUN,否则 GHA/BuildKit 可能单独命中 composer 相关层缓存,vendor 仍来自旧构建
|
||||
ARG CACHE_BUST=local
|
||||
|
||||
# Add application
|
||||
RUN mkdir -p /usr/src && wget --no-cache https://github.com/netcccyun/dnsmgr/archive/refs/heads/main.zip -O /usr/src/www.zip && unzip /usr/src/www.zip -d /usr/src/ && mv /usr/src/dnsmgr-main /usr/src/www && rm -f /usr/src/www.zip
|
||||
RUN mkdir -p /usr/src && echo "$CACHE_BUST" >/dev/null && wget --no-cache https://github.com/netcccyun/dnsmgr/archive/refs/heads/main.zip -O /usr/src/www.zip && unzip /usr/src/www.zip -d /usr/src/ && mv /usr/src/dnsmgr-main /usr/src/www && rm -f /usr/src/www.zip
|
||||
|
||||
# Install composer
|
||||
RUN wget https://getcomposer.org/download/latest-stable/composer.phar -O /usr/local/bin/composer && chmod +x /usr/local/bin/composer
|
||||
# Install composer(与下面 install 一并随 CACHE_BUST 失效)
|
||||
RUN echo "$CACHE_BUST" >/dev/null && wget https://getcomposer.org/download/latest-stable/composer.phar -O /usr/local/bin/composer && chmod +x /usr/local/bin/composer
|
||||
|
||||
RUN composer install -d /usr/src/www --no-interaction --no-dev --optimize-autoloader
|
||||
RUN echo "$CACHE_BUST" >/dev/null && composer install -d /usr/src/www --no-interaction --no-dev --optimize-autoloader --no-cache
|
||||
|
||||
RUN adduser -D -s /sbin/nologin -g www www && chown -R www.www /usr/src/www /var/lib/nginx /var/log/nginx
|
||||
|
||||
|
||||
3
.github/workflows/docker-build.yml
vendored
3
.github/workflows/docker-build.yml
vendored
@@ -46,6 +46,9 @@ jobs:
|
||||
file: .github/docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
# 每次运行唯一,打破「下载源码 + composer」等层的缓存,否则会一直用首次构建时的层
|
||||
build-args: |
|
||||
CACHE_BUST=${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
# 避免向仓库推送 attestations;部分镜像仓库(含部分 SWR 场景)无法解析导致 “fail to parse manifest.json”
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
@@ -751,7 +751,7 @@ class Cert extends BaseController
|
||||
$ids = input('post.ids');
|
||||
$success = 0;
|
||||
$certid = 0;
|
||||
if (input('post.action') == 'cert') {
|
||||
if (input('post.act') == 'cert') {
|
||||
$certid = input('post.certid/d');
|
||||
$cert = Db::name('cert_order')->where('id', $certid)->find();
|
||||
if (!$cert) return json(['code' => -1, 'msg' => '证书订单不存在']);
|
||||
|
||||
@@ -43,15 +43,23 @@ class Cloudflare extends BaseController
|
||||
$context = $this->getCloudflareDomainContext(input('param.id/d'));
|
||||
$hostname = trim(input('post.hostname', '', 'trim'));
|
||||
$origin = trim(input('post.custom_origin_server', '', 'trim'));
|
||||
$sslMethod = trim(input('post.ssl_method', 'txt', 'trim'));
|
||||
$minTlsVersion = trim(input('post.min_tls_version', '1.0', 'trim'));
|
||||
if (empty($hostname) || !checkDomain($hostname)) {
|
||||
throw new Exception('主机名格式不正确');
|
||||
}
|
||||
if (!in_array($sslMethod, ['txt', 'http'])) {
|
||||
throw new Exception('证书验证方法无效');
|
||||
}
|
||||
if (!in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
|
||||
throw new Exception('最低 TLS 版本无效');
|
||||
}
|
||||
if ($origin !== '') {
|
||||
$this->validateCustomOrigin($origin);
|
||||
}
|
||||
|
||||
$result = $context['service']->createCustomHostname($context['domain']['thirdid'], $hostname, $origin !== '' ? $origin : null);
|
||||
$this->add_log($context['domain']['name'], '创建自定义主机名', $hostname . ($origin !== '' ? ' -> ' . $origin : ''));
|
||||
$result = $context['service']->createCustomHostname($context['domain']['thirdid'], $hostname, $origin !== '' ? $origin : null, $sslMethod, $minTlsVersion);
|
||||
$this->add_log($context['domain']['name'], '创建自定义主机名', $hostname . ($origin !== '' ? ' -> ' . $origin : '') . ' (验证: ' . $sslMethod . ', TLS: ' . $minTlsVersion . ')');
|
||||
return json(['code' => 0, 'msg' => '创建自定义主机名成功', 'data' => $this->formatCustomHostnameRow($result)]);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
@@ -70,6 +78,14 @@ class Cloudflare extends BaseController
|
||||
$current = $context['service']->getCustomHostname($context['domain']['thirdid'], $hostnameId);
|
||||
$hostname = trim((string)($current['hostname'] ?? ''));
|
||||
$origin = trim(input('post.custom_origin_server', '', 'trim'));
|
||||
$sslMethod = trim(input('post.ssl_method', 'txt', 'trim'));
|
||||
$minTlsVersion = trim(input('post.min_tls_version', '1.0', 'trim'));
|
||||
if (!in_array($sslMethod, ['txt', 'http'])) {
|
||||
throw new Exception('证书验证方法无效');
|
||||
}
|
||||
if (!in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
|
||||
throw new Exception('最低 TLS 版本无效');
|
||||
}
|
||||
if ($origin !== '') {
|
||||
$this->validateCustomOrigin($origin);
|
||||
}
|
||||
@@ -79,10 +95,10 @@ class Cloudflare extends BaseController
|
||||
$hostnameId,
|
||||
[
|
||||
'custom_origin_server' => $origin !== '' ? $origin : null,
|
||||
'ssl' => $this->extractCustomHostnameSslPayload($current),
|
||||
'ssl' => $this->extractCustomHostnameSslPayload($current, $sslMethod, $minTlsVersion),
|
||||
]
|
||||
);
|
||||
$this->add_log($context['domain']['name'], '编辑自定义主机名', $hostname . ' -> ' . ($origin !== '' ? $origin : '清空源站'));
|
||||
$this->add_log($context['domain']['name'], '编辑自定义主机名', $hostname . ' -> ' . ($origin !== '' ? $origin : '清空源站') . ' (验证: ' . $sslMethod . ', TLS: ' . $minTlsVersion . ')');
|
||||
return json(['code' => 0, 'msg' => '更新自定义主机名成功', 'data' => $this->formatCustomHostnameRow($result)]);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
@@ -109,8 +125,8 @@ class Cloudflare extends BaseController
|
||||
'ssl' => $this->extractCustomHostnameSslPayload($current),
|
||||
]
|
||||
);
|
||||
$this->add_log($context['domain']['name'], '刷新自定义主机名校验', $hostname);
|
||||
return json(['code' => 0, 'msg' => '已向 Cloudflare 重新发起校验', 'data' => $this->formatCustomHostnameRow($result)]);
|
||||
$this->add_log($context['domain']['name'], '刷新自定义主机名验证', $hostname);
|
||||
return json(['code' => 0, 'msg' => '已向 Cloudflare 重新发起验证', 'data' => $this->formatCustomHostnameRow($result)]);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
@@ -125,15 +141,170 @@ class Cloudflare extends BaseController
|
||||
if ($hostnameId === '') {
|
||||
throw new Exception('缺少 hostname_id');
|
||||
}
|
||||
|
||||
$context['service']->deleteCustomHostname($context['domain']['thirdid'], $hostnameId);
|
||||
$this->add_log($context['domain']['name'], '删除自定义主机名', $hostname !== '' ? $hostname : $hostnameId);
|
||||
$this->add_log($context['domain']['name'], '删除自定义主机名', $hostname);
|
||||
return json(['code' => 0, 'msg' => '删除自定义主机名成功']);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function hostnames_batch_delete()
|
||||
{
|
||||
try {
|
||||
$context = $this->getCloudflareDomainContext(input('param.id/d'));
|
||||
$hostnameIds = input('post.hostname_ids/a', []);
|
||||
if (empty($hostnameIds)) {
|
||||
throw new Exception('缺少 hostname_ids');
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
foreach ($hostnameIds as $hostnameId) {
|
||||
if (trim((string)$hostnameId) !== '') {
|
||||
try {
|
||||
// 获取主机名信息用于日志
|
||||
$hostnameInfo = $context['service']->getCustomHostname($context['domain']['thirdid'], trim((string)$hostnameId));
|
||||
$hostname = trim((string)($hostnameInfo['hostname'] ?? ''));
|
||||
|
||||
$context['service']->deleteCustomHostname($context['domain']['thirdid'], trim((string)$hostnameId));
|
||||
$deletedCount++;
|
||||
// 为每个成功删除的主机名记录单独的日志
|
||||
$this->add_log($context['domain']['name'], '批量删除自定义主机名', $hostname);
|
||||
} catch (Exception $e) {
|
||||
// 忽略删除失败的情况,继续处理其他主机名
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return json(['code' => 0, 'msg' => '批量删除成功,共删除 ' . $deletedCount . ' 个自定义主机名']);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function hostnames_batch_update()
|
||||
{
|
||||
try {
|
||||
$context = $this->getCloudflareDomainContext(input('param.id/d'));
|
||||
$hostnameIds = input('post.hostname_ids/s', '');
|
||||
$hostnameIdArray = array_filter(array_map('trim', explode(',', $hostnameIds)));
|
||||
if (empty($hostnameIdArray)) {
|
||||
throw new Exception('缺少 hostname_ids');
|
||||
}
|
||||
|
||||
$origin = trim(input('post.custom_origin_server', '', 'trim'));
|
||||
$sslMethod = trim(input('post.ssl_method', '', 'trim'));
|
||||
$minTlsVersion = trim(input('post.min_tls_version', '', 'trim'));
|
||||
|
||||
if (!empty($sslMethod) && !in_array($sslMethod, ['txt', 'http'])) {
|
||||
throw new Exception('证书验证方法无效');
|
||||
}
|
||||
if (!empty($minTlsVersion) && !in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
|
||||
throw new Exception('最低 TLS 版本无效');
|
||||
}
|
||||
if ($origin !== '') {
|
||||
$this->validateCustomOrigin($origin);
|
||||
}
|
||||
|
||||
$updatedCount = 0;
|
||||
foreach ($hostnameIdArray as $hostnameId) {
|
||||
if (trim((string)$hostnameId) !== '') {
|
||||
try {
|
||||
$current = $context['service']->getCustomHostname($context['domain']['thirdid'], $hostnameId);
|
||||
$hostname = trim((string)($current['hostname'] ?? ''));
|
||||
$payload = [];
|
||||
|
||||
// 总是设置 custom_origin_server,留空时设置为 null 表示清空
|
||||
$payload['custom_origin_server'] = $origin !== '' ? $origin : null;
|
||||
|
||||
if (!empty($sslMethod) || !empty($minTlsVersion)) {
|
||||
$payload['ssl'] = $this->extractCustomHostnameSslPayload($current, $sslMethod, $minTlsVersion);
|
||||
}
|
||||
|
||||
if (!empty($payload)) {
|
||||
$context['service']->updateCustomHostname($context['domain']['thirdid'], $hostnameId, $payload);
|
||||
$updatedCount++;
|
||||
// 为每个成功修改的主机名记录单独的日志
|
||||
$logMessage = $hostname . ' -> ' . ($origin !== '' ? $origin : '清空源站') . ' (验证: ' . ($sslMethod ?: '保持不变') . ', TLS: ' . ($minTlsVersion ?: '保持不变') . ')';
|
||||
$this->add_log($context['domain']['name'], '批量修改自定义主机名', $logMessage);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// 忽略修改失败的情况,继续处理其他主机名
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return json(['code' => 0, 'msg' => '批量修改成功,共修改 ' . $updatedCount . ' 个自定义主机名']);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function hostnames_batch_add()
|
||||
{
|
||||
try {
|
||||
$context = $this->getCloudflareDomainContext(input('param.id/d'));
|
||||
$hostnamesText = trim(input('post.hostnames', '', 'trim'));
|
||||
$origin = trim(input('post.custom_origin_server', '', 'trim'));
|
||||
$sslMethod = trim(input('post.ssl_method', 'txt', 'trim'));
|
||||
$minTlsVersion = trim(input('post.min_tls_version', '1.0', 'trim'));
|
||||
|
||||
if (empty($hostnamesText)) {
|
||||
throw new Exception('缺少主机名列表');
|
||||
}
|
||||
if (!in_array($sslMethod, ['txt', 'http'])) {
|
||||
throw new Exception('证书验证方法无效');
|
||||
}
|
||||
if (!in_array($minTlsVersion, ['1.0', '1.1', '1.2', '1.3'])) {
|
||||
throw new Exception('最低 TLS 版本无效');
|
||||
}
|
||||
if ($origin !== '') {
|
||||
$this->validateCustomOrigin($origin);
|
||||
}
|
||||
|
||||
$hostnames = array_filter(array_map('trim', explode("\n", $hostnamesText)));
|
||||
if (empty($hostnames)) {
|
||||
throw new Exception('主机名列表为空');
|
||||
}
|
||||
|
||||
$addedCount = 0;
|
||||
$failedHostnames = [];
|
||||
foreach ($hostnames as $hostname) {
|
||||
if (empty($hostname)) {
|
||||
continue;
|
||||
}
|
||||
if (!checkDomain($hostname)) {
|
||||
$failedHostnames[] = $hostname . '(格式不正确)';
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$context['service']->createCustomHostname(
|
||||
$context['domain']['thirdid'],
|
||||
$hostname,
|
||||
$origin !== '' ? $origin : null,
|
||||
$sslMethod,
|
||||
$minTlsVersion
|
||||
);
|
||||
$addedCount++;
|
||||
// 为每个成功添加的主机名记录单独的日志
|
||||
$logMessage = $hostname . ($origin !== '' ? ' -> ' . $origin : '') . ' (验证: ' . $sslMethod . ', TLS: ' . $minTlsVersion . ')';
|
||||
$this->add_log($context['domain']['name'], '批量添加自定义主机名', $logMessage);
|
||||
} catch (Exception $e) {
|
||||
$failedHostnames[] = $hostname . '(' . $e->getMessage() . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$message = '批量添加成功,共添加 ' . $addedCount . ' 个自定义主机名';
|
||||
if (!empty($failedHostnames)) {
|
||||
$message .= ',失败 ' . count($failedHostnames) . ' 个:' . implode('; ', $failedHostnames);
|
||||
}
|
||||
|
||||
return json(['code' => 0, 'msg' => $message]);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function hostnames_txt_targets()
|
||||
{
|
||||
try {
|
||||
@@ -196,6 +367,96 @@ class Cloudflare extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function dcv_delegation_uuid()
|
||||
{
|
||||
try {
|
||||
$context = $this->getCloudflareDomainContext(input('param.id/d'));
|
||||
$uuid = $context['service']->getDcvDelegationUuid($context['domain']['thirdid']);
|
||||
return json(['code' => 0, 'data' => ['uuid' => $uuid]]);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function get_domain_default_line()
|
||||
{
|
||||
try {
|
||||
$domainId = input('param.domain_id/d');
|
||||
if (empty($domainId)) {
|
||||
throw new Exception('缺少 domain_id 参数');
|
||||
}
|
||||
|
||||
// 查询域名信息
|
||||
$domainRow = Db::name('domain')->alias('A')
|
||||
->join('account B', 'A.aid = B.id')
|
||||
->where('A.id', $domainId)
|
||||
->field('A.*, B.type, B.config account_config')
|
||||
->find();
|
||||
|
||||
if (!$domainRow) {
|
||||
throw new Exception('域名不存在');
|
||||
}
|
||||
|
||||
// 获取该域名的默认线路
|
||||
$recordLine = cache('record_line_' . $domainId);
|
||||
|
||||
if (empty($recordLine)) {
|
||||
// 缓存中没有,需要从 DNS 提供商获取
|
||||
$config = json_decode($domainRow['account_config'] ?? '', true);
|
||||
if (!is_array($config)) {
|
||||
$config = [];
|
||||
}
|
||||
|
||||
$dnsModel = \app\lib\DnsHelper::getModel(
|
||||
intval($domainRow['aid']),
|
||||
$domainRow['name'],
|
||||
$domainRow['thirdid'],
|
||||
$domainRow['type'],
|
||||
$config
|
||||
);
|
||||
|
||||
if ($dnsModel && method_exists($dnsModel, 'getRecordLine')) {
|
||||
$recordLine = $dnsModel->getRecordLine();
|
||||
if ($recordLine && is_array($recordLine)) {
|
||||
cache('record_line_' . $domainId, $recordLine, 604800); // 缓存7天
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($recordLine) || !is_array($recordLine)) {
|
||||
throw new Exception('无法获取该域名的解析线路列表');
|
||||
}
|
||||
|
||||
$firstKey = array_key_first($recordLine);
|
||||
if ($firstKey === null) {
|
||||
throw new Exception('解析线路列表为空');
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
foreach ($recordLine as $lineValue => $lineLabel) {
|
||||
if (is_array($lineLabel)) {
|
||||
$lines[] = [
|
||||
'value' => strval($lineValue),
|
||||
'label' => isset($lineLabel['name']) ? strval($lineLabel['name']) : strval($lineValue),
|
||||
'parent' => isset($lineLabel['parent']) ? ($lineLabel['parent'] !== null ? strval($lineLabel['parent']) : '') : '',
|
||||
'is_default' => ($lineValue === $firstKey)
|
||||
];
|
||||
} else {
|
||||
$lines[] = [
|
||||
'value' => strval($lineValue),
|
||||
'label' => strval($lineLabel),
|
||||
'parent' => '',
|
||||
'is_default' => ($lineValue === $firstKey)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return json(['code' => 0, 'data' => ['default_line' => strval($firstKey), 'lines' => $lines]]);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function tunnels()
|
||||
{
|
||||
try {
|
||||
@@ -650,11 +911,11 @@ class Cloudflare extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
private function extractCustomHostnameSslPayload(array $row): array
|
||||
private function extractCustomHostnameSslPayload(array $row, string $sslMethod = '', string $minTlsVersion = ''): array
|
||||
{
|
||||
$ssl = isset($row['ssl']) && is_array($row['ssl']) ? $row['ssl'] : [];
|
||||
$payload = [
|
||||
'method' => trim((string)($ssl['method'] ?? 'http')),
|
||||
'method' => $sslMethod !== '' ? $sslMethod : trim((string)($ssl['method'] ?? 'http')),
|
||||
'type' => trim((string)($ssl['type'] ?? 'dv')),
|
||||
];
|
||||
if ($payload['method'] === '') {
|
||||
@@ -663,6 +924,16 @@ class Cloudflare extends BaseController
|
||||
if ($payload['type'] === '') {
|
||||
$payload['type'] = 'dv';
|
||||
}
|
||||
|
||||
// 添加 TLS 版本设置
|
||||
if ($minTlsVersion !== '') {
|
||||
$payload['settings'] = [
|
||||
'min_tls_version' => $minTlsVersion
|
||||
];
|
||||
} elseif (isset($ssl['settings']) && is_array($ssl['settings'])) {
|
||||
$payload['settings'] = $ssl['settings'];
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
@@ -755,8 +1026,10 @@ class Cloudflare extends BaseController
|
||||
'hostname' => trim((string)($row['hostname'] ?? '')),
|
||||
'custom_origin_server' => trim((string)($row['custom_origin_server'] ?? '')),
|
||||
'status' => trim((string)($row['status'] ?? '')),
|
||||
'ssl' => $ssl,
|
||||
'ssl_status' => trim((string)($ssl['status'] ?? '')),
|
||||
'ssl_method' => trim((string)($ssl['method'] ?? '')),
|
||||
'ssl_min_tls_version' => trim((string)($ssl['settings']['min_tls_version'] ?? '')),
|
||||
'ssl_type' => trim((string)($ssl['type'] ?? '')),
|
||||
'ssl_validation_status' => $sslValidationStatus,
|
||||
'verification_status' => $verificationStatus !== '' ? $verificationStatus : '-',
|
||||
|
||||
@@ -8,6 +8,7 @@ use think\facade\View;
|
||||
use think\facade\Cache;
|
||||
use app\lib\DnsHelper;
|
||||
use app\service\ExpireNoticeService;
|
||||
use app\utils\DnsQueryUtils;
|
||||
use Exception;
|
||||
|
||||
class Domain extends BaseController
|
||||
@@ -157,8 +158,10 @@ class Domain extends BaseController
|
||||
}
|
||||
$accounts[] = ['id' => $row['id'], 'name' => $name, 'type' => DnsHelper::$dns_config[$row['type']]['name'], 'add' => DnsHelper::$dns_config[$row['type']]['add']];
|
||||
}
|
||||
$categorys = Db::name('domain_category')->order('sort', 'asc')->order('id', 'desc')->select();
|
||||
View::assign('accounts', $accounts);
|
||||
View::assign('types', $types);
|
||||
View::assign('categorys', $categorys);
|
||||
return view();
|
||||
}
|
||||
|
||||
@@ -188,6 +191,7 @@ class Domain extends BaseController
|
||||
$kw = input('post.kw', null, 'trim');
|
||||
$type = input('post.type', null, 'trim');
|
||||
$status = input('post.status', null, 'trim');
|
||||
$cid = input('post.cid', null, 'trim');
|
||||
$order = input('post.order', null, 'trim');
|
||||
$offset = input('post.offset/d', 0);
|
||||
$limit = input('post.limit/d', 10);
|
||||
@@ -206,6 +210,9 @@ class Domain extends BaseController
|
||||
if (!empty($type)) {
|
||||
$select->whereLike('B.type', $type);
|
||||
}
|
||||
if (!isNullOrEmpty($cid)) {
|
||||
$select->where('A.cid', $cid);
|
||||
}
|
||||
if (request()->user['level'] == 1) {
|
||||
$select->where('is_hide', 0)->where('A.name', 'in', request()->user['permission']);
|
||||
}
|
||||
@@ -235,10 +242,12 @@ class Domain extends BaseController
|
||||
}
|
||||
$rows = $select->fieldRaw('A.*,B.type,B.remark aremark')->limit($offset, $limit)->select();
|
||||
|
||||
$categorys = Db::name('domain_category')->column('name', 'id');
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$row['typename'] = DnsHelper::$dns_config[$row['type']]['name'];
|
||||
$row['icon'] = DnsHelper::$dns_config[$row['type']]['icon'];
|
||||
$row['category_name'] = isset($categorys[$row['cid']]) ? $categorys[$row['cid']] : '';
|
||||
$list[] = $row;
|
||||
}
|
||||
|
||||
@@ -290,6 +299,7 @@ class Domain extends BaseController
|
||||
$is_hide = input('post.is_hide/d');
|
||||
$is_sso = input('post.is_sso/d');
|
||||
$is_notice = input('post.is_notice/d');
|
||||
$cid = input('post.cid/d', 0);
|
||||
$expiretime = input('post.expiretime', null, 'trim');
|
||||
$remark = input('post.remark', null, 'trim');
|
||||
if (empty($remark)) $remark = null;
|
||||
@@ -297,6 +307,7 @@ class Domain extends BaseController
|
||||
'is_hide' => $is_hide,
|
||||
'is_sso' => $is_sso,
|
||||
'is_notice' => $is_notice,
|
||||
'cid' => $cid,
|
||||
'expiretime' => $expiretime ? $expiretime : null,
|
||||
'remark' => $remark,
|
||||
]);
|
||||
@@ -1005,6 +1016,68 @@ class Domain extends BaseController
|
||||
return view('log');
|
||||
}
|
||||
|
||||
public function smartparse()
|
||||
{
|
||||
if (request()->user['type'] == 'domain') {
|
||||
return redirect('/record/' . request()->user['id']);
|
||||
}
|
||||
|
||||
$list = Db::name('domain')->alias('A')->join('account B', 'A.aid = B.id')
|
||||
->field('A.id, A.name, A.aid, B.type')
|
||||
->order('A.name', 'asc')
|
||||
->select();
|
||||
|
||||
$domainList = [];
|
||||
foreach ($list as $row) {
|
||||
if (request()->user['level'] == 1 && !in_array($row['name'], request()->user['permission'])) {
|
||||
continue;
|
||||
}
|
||||
$dnsTypeName = isset(DnsHelper::$dns_config[$row['type']]) ? DnsHelper::$dns_config[$row['type']]['name'] : $row['type'];
|
||||
$domainList[] = [
|
||||
'id' => $row['id'],
|
||||
'name' => $row['name'],
|
||||
'dnsType' => $dnsTypeName
|
||||
];
|
||||
}
|
||||
|
||||
View::assign('domainList', $domainList);
|
||||
return view();
|
||||
}
|
||||
|
||||
public function quickinfo()
|
||||
{
|
||||
$id = input('param.id/d');
|
||||
$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' => '无权限']);
|
||||
|
||||
try {
|
||||
list($recordLine, $minTTL) = $this->get_line_and_ttl($drow);
|
||||
|
||||
$recordLineArr = [];
|
||||
foreach ($recordLine as $key => $item) {
|
||||
$recordLineArr[] = ['id' => strval($key), 'name' => $item['name'], 'parent' => $item['parent']];
|
||||
}
|
||||
|
||||
$dnstype = Db::name('account')->where('id', $drow['aid'])->value('type');
|
||||
$dnsconfig = DnsHelper::$dns_config[$dnstype];
|
||||
|
||||
return json([
|
||||
'code' => 0,
|
||||
'data' => [
|
||||
'recordLine' => $recordLineArr,
|
||||
'minTTL' => $minTTL ? $minTTL : 1,
|
||||
'weight' => $dnsconfig['weight'] ?? false,
|
||||
'remark' => $dnsconfig['remark'] ?? 0
|
||||
]
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return json(['code' => -1, 'msg' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function add_log($domain, $action, $data)
|
||||
{
|
||||
if (strlen($data) > 500) $data = substr($data, 0, 500);
|
||||
@@ -1218,4 +1291,138 @@ class Domain extends BaseController
|
||||
$result = (new ExpireNoticeService())->updateDomainDate($id, $drow['name']);
|
||||
return json($result);
|
||||
}
|
||||
|
||||
public function record_check()
|
||||
{
|
||||
$id = input('param.id/d');
|
||||
$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' => '无权限']);
|
||||
|
||||
$recordid = input('post.recordid', null, 'trim');
|
||||
$name = input('post.name', null, 'trim');
|
||||
$type = input('post.type', null, 'trim');
|
||||
$value = input('post.value', null, 'trim');
|
||||
|
||||
if (empty($recordid) || empty($name) || empty($type)) {
|
||||
return json(['code' => -1, 'msg' => '参数不能为空']);
|
||||
}
|
||||
|
||||
$domain = $name === '@' ? $drow['name'] : $name . '.' . $drow['name'];
|
||||
$domain = strtolower($domain);
|
||||
|
||||
$supported_types = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'SRV', 'CAA', 'PTR', 'LOC', 'LUA'];
|
||||
if (!in_array($type, $supported_types)) {
|
||||
return json(['code' => -1, 'msg' => '该记录类型暂不支持检测']);
|
||||
}
|
||||
|
||||
$dns_records = DnsQueryUtils::get_dns_records($domain, $type);
|
||||
if ($dns_records === false || empty($dns_records)) {
|
||||
$dns_records = DnsQueryUtils::query_dns_doh($domain, $type);
|
||||
}
|
||||
|
||||
if ($dns_records === false || empty($dns_records)) {
|
||||
return json(['code' => 0, 'data' => ['status' => 'not_found', 'message' => '未查询到该解析记录', 'actual' => []]]);
|
||||
}
|
||||
|
||||
$dns_records = array_map('strtolower', $dns_records);
|
||||
$expected_value = strtolower(rtrim(trim($value), '.'));
|
||||
|
||||
if (in_array($expected_value, $dns_records)) {
|
||||
return json(['code' => 0, 'data' => ['status' => 'active', 'actual' => $dns_records]]);
|
||||
} else {
|
||||
return json(['code' => 0, 'data' => ['status' => 'mismatch', 'expected' => $expected_value, 'actual' => $dns_records]]);
|
||||
}
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
return view();
|
||||
}
|
||||
|
||||
public function category_data()
|
||||
{
|
||||
if (!checkPermission(2)) return json(['total' => 0, 'rows' => []]);
|
||||
$offset = input('post.offset/d', 0);
|
||||
$limit = input('post.limit/d', 10);
|
||||
|
||||
$select = Db::name('domain_category');
|
||||
$total = $select->count();
|
||||
$rows = $select->order('sort', 'asc')->order('id', 'desc')->limit($offset, $limit)->select()->toArray();
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$row['domain_count'] = Db::name('domain')->where('cid', $row['id'])->count();
|
||||
}
|
||||
|
||||
return json(['total' => $total, 'rows' => $rows]);
|
||||
}
|
||||
|
||||
public function category_op()
|
||||
{
|
||||
if (!checkPermission(2)) return json(['code' => -1, 'msg' => '无权限']);
|
||||
$action = input('param.action');
|
||||
if ($action == 'add') {
|
||||
$name = input('post.name', null, 'trim');
|
||||
$remark = input('post.remark', null, 'trim');
|
||||
$sort = input('post.sort/d', 0);
|
||||
if (empty($name)) return json(['code' => -1, 'msg' => '分类名称不能为空']);
|
||||
if (Db::name('domain_category')->where('name', $name)->find()) {
|
||||
return json(['code' => -1, 'msg' => '分类名称已存在']);
|
||||
}
|
||||
Db::name('domain_category')->insert([
|
||||
'name' => $name,
|
||||
'remark' => $remark,
|
||||
'sort' => $sort,
|
||||
'addtime' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
return json(['code' => 0, 'msg' => '添加分类成功!']);
|
||||
} elseif ($action == 'edit') {
|
||||
$id = input('post.id/d');
|
||||
$row = Db::name('domain_category')->where('id', $id)->find();
|
||||
if (!$row) return json(['code' => -1, 'msg' => '分类不存在']);
|
||||
$name = input('post.name', null, 'trim');
|
||||
$remark = input('post.remark', null, 'trim');
|
||||
$sort = input('post.sort/d', 0);
|
||||
if (empty($name)) return json(['code' => -1, 'msg' => '分类名称不能为空']);
|
||||
if (Db::name('domain_category')->where('name', $name)->where('id', '<>', $id)->find()) {
|
||||
return json(['code' => -1, 'msg' => '分类名称已存在']);
|
||||
}
|
||||
Db::name('domain_category')->where('id', $id)->update([
|
||||
'name' => $name,
|
||||
'remark' => $remark,
|
||||
'sort' => $sort,
|
||||
]);
|
||||
return json(['code' => 0, 'msg' => '修改分类成功!']);
|
||||
} elseif ($action == 'del') {
|
||||
$id = input('post.id/d');
|
||||
$count = Db::name('domain')->where('cid', $id)->count();
|
||||
if ($count > 0) return json(['code' => -1, 'msg' => '该分类下存在域名,无法删除']);
|
||||
Db::name('domain_category')->where('id', $id)->delete();
|
||||
return json(['code' => 0, 'msg' => '删除分类成功!']);
|
||||
}
|
||||
return json(['code' => -3]);
|
||||
}
|
||||
|
||||
public function category_list()
|
||||
{
|
||||
if (!checkPermission(2)) return json(['code' => -1, 'msg' => '无权限']);
|
||||
$list = Db::name('domain_category')->order('sort', 'asc')->order('id', 'desc')->select();
|
||||
foreach ($list as &$row) {
|
||||
$row['domain_count'] = Db::name('domain')->where('cid', $row['id'])->count();
|
||||
}
|
||||
return json(['code' => 0, 'data' => $list]);
|
||||
}
|
||||
|
||||
public function domain_set_category()
|
||||
{
|
||||
if (!checkPermission(2)) return json(['code' => -1, 'msg' => '无权限']);
|
||||
$ids = input('post.ids');
|
||||
$cid = input('post.cid/d', 0);
|
||||
if (empty($ids)) return json(['code' => -1, 'msg' => '请选择要操作的域名']);
|
||||
$count = Db::name('domain')->where('id', 'in', $ids)->update(['cid' => $cid]);
|
||||
return json(['code' => 0, 'msg' => '成功设置' . $count . '个域名的分类!']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,20 @@ class System extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function customwebhooktest()
|
||||
{
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
$custom_webhook_url = config_get('custom_webhook_url');
|
||||
if (empty($custom_webhook_url)) return json(['code' => -1, 'msg' => '请先保存设置']);
|
||||
$content = "这是一封测试消息!\n来自:" . $this->request->root(true);
|
||||
$result = \app\utils\MsgNotice::send_custom_webhook('消息发送测试', $content);
|
||||
if ($result === true) {
|
||||
return json(['code' => 0, 'msg' => '消息发送成功!']);
|
||||
} else {
|
||||
return json(['code' => -1, 'msg' => '消息发送失败!' . $result]);
|
||||
}
|
||||
}
|
||||
|
||||
public function proxytest()
|
||||
{
|
||||
if (!checkPermission(2)) return $this->alert('error', '无权限');
|
||||
|
||||
@@ -296,6 +296,59 @@ class DeployHelper
|
||||
],
|
||||
'taskinputs' => [],
|
||||
],
|
||||
'nginxproxymanager' => [
|
||||
'name' => 'Nginx Proxy Manager',
|
||||
'class' => 1,
|
||||
'icon' => 'npm.svg',
|
||||
'desc' => '更新 Nginx Proxy Manager 的自定义证书并自动绑定 Proxy Host',
|
||||
'note' => '填写 Nginx Proxy Manager 面板地址与登录账号密码,系统将通过官方 API 登录并执行证书更新。',
|
||||
'tasknote' => '如填写证书ID则优先更新该自定义证书;留空时系统会根据当前证书订单的域名在 NPM 中匹配 Proxy Host,并在首次成功后自动保存证书ID,后续续期优先走该ID,不再依赖域名匹配。',
|
||||
'inputs' => [
|
||||
'url' => [
|
||||
'name' => '面板地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'Nginx Proxy Manager 面板地址',
|
||||
'note' => '填写规则如:http://192.168.1.100:81 ,不要带 /api 等后缀',
|
||||
'required' => true,
|
||||
],
|
||||
'email' => [
|
||||
'name' => '登录邮箱',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'NPM 登录邮箱',
|
||||
'validator' => 'email',
|
||||
'required' => true,
|
||||
],
|
||||
'password' => [
|
||||
'name' => '登录密码',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'NPM 登录密码',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'taskinputs' => [
|
||||
'id' => [
|
||||
'name' => '证书ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '留空则按域名匹配 Proxy Host 并自动回填',
|
||||
'note' => '优先级最高。填写后将直接更新该自定义证书ID;仅支持 NPM 中 provider 为 other 的自定义证书。',
|
||||
],
|
||||
'host_id' => [
|
||||
'name' => 'Proxy Host ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '可留空,留空则按域名自动匹配',
|
||||
'note' => '可选。未填写证书ID时,若填写此项则仅处理指定 Proxy Host;若留空则按当前证书订单域名自动查找匹配的 Proxy Host。',
|
||||
],
|
||||
],
|
||||
],
|
||||
'btwaf' => [
|
||||
'name' => '堡塔云WAF',
|
||||
'class' => 1,
|
||||
@@ -1198,6 +1251,7 @@ ctrl+x 保存退出<br/>',
|
||||
['value'=>'esa_saas', 'label'=>'边缘安全加速ESA SaaS'],
|
||||
['value'=>'oss', 'label'=>'对象存储OSS'],
|
||||
['value'=>'waf', 'label'=>'Web应用防火墙3.0'],
|
||||
['value'=>'wafres', 'label'=>'Web应用防火墙3.0(云产品接入)'],
|
||||
['value'=>'waf2', 'label'=>'Web应用防火墙2.0'],
|
||||
['value'=>'clb', 'label'=>'传统型负载均衡CLB'],
|
||||
['value'=>'alb', 'label'=>'应用型负载均衡ALB'],
|
||||
@@ -1250,7 +1304,7 @@ ctrl+x 保存退出<br/>',
|
||||
['value'=>'ap-southeast-1', 'label'=>'非中国内地'],
|
||||
],
|
||||
'value' => 'cn-hangzhou',
|
||||
'show' => 'product==\'waf\'||product==\'waf2\'||product==\'ddoscoo\'||product==\'esa\'||product==\'esa_saas\'',
|
||||
'show' => 'product==\'waf\'||product==\'waf2\'||product==\'wafres\'||product==\'ddoscoo\'||product==\'esa\'||product==\'esa_saas\'',
|
||||
'required' => true,
|
||||
],
|
||||
'regionid' => [
|
||||
@@ -1321,6 +1375,14 @@ ctrl+x 保存退出<br/>',
|
||||
'note' => '进入实例详情->监听列表,复制监听ID(只支持HTTPS监听协议)',
|
||||
'required' => true,
|
||||
],
|
||||
'waf_resource_id' => [
|
||||
'name' => '云产品防护对象ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '多个ID可用,隔开',
|
||||
'show' => 'product==\'wafres\'',
|
||||
'note' => '进入查看防护对象,对象名称一列即为云产品防护对象ID',
|
||||
'required' => true,
|
||||
],
|
||||
'deploy_type' => [
|
||||
'name' => '部署证书类型',
|
||||
'type' => 'select',
|
||||
@@ -1329,7 +1391,7 @@ ctrl+x 保存退出<br/>',
|
||||
['value'=>'1', 'label'=>'扩展证书'],
|
||||
],
|
||||
'value' => '0',
|
||||
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'||product==\'ga\'',
|
||||
'show' => 'product==\'clb\'||product==\'alb\'||product==\'nlb\'||product==\'ga\'||product==\'wafres\'',
|
||||
'required' => true,
|
||||
],
|
||||
'clb_domain' => [
|
||||
@@ -1343,7 +1405,7 @@ ctrl+x 保存退出<br/>',
|
||||
'name' => '绑定的域名',
|
||||
'type' => 'input',
|
||||
'placeholder' => '多个域名可用,隔开',
|
||||
'show' => 'product!=\'esa\'&&product!=\'esa_saas\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'&&product!=\'ga\'&&product!=\'upload\'',
|
||||
'show' => 'product!=\'esa\'&&product!=\'esa_saas\'&&product!=\'clb\'&&product!=\'alb\'&&product!=\'nlb\'&&product!=\'ga\'&&product!=\'upload\'&&product!=\'wafres\'',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -518,6 +518,41 @@ class DnsHelper
|
||||
'page' => true,
|
||||
'add' => true,
|
||||
],
|
||||
'technitium' => [
|
||||
'name' => 'Technitium',
|
||||
'icon' => 'technitium.png',
|
||||
'note' => '',
|
||||
'config' => [
|
||||
'url' => [
|
||||
'name' => 'Server URL',
|
||||
'type' => 'input',
|
||||
'placeholder' => 'http://127.0.0.1:5380',
|
||||
'required' => true,
|
||||
],
|
||||
'token' => [
|
||||
'name' => 'API Token',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => true,
|
||||
'redirect' => false,
|
||||
'log' => false,
|
||||
'weight' => false,
|
||||
'page' => true,
|
||||
'add' => true,
|
||||
],
|
||||
'aliyunesa' => [
|
||||
'name' => '阿里云ESA',
|
||||
'icon' => 'aliyun.png',
|
||||
@@ -608,6 +643,47 @@ class DnsHelper
|
||||
'page' => false,
|
||||
'add' => false,
|
||||
],
|
||||
'dnsmgr' => [
|
||||
'name' => '同系统对接',
|
||||
'icon' => 'logo.png',
|
||||
'note' => '对接其他聚合DNS管理系统站点',
|
||||
'config' => [
|
||||
'base_url' => [
|
||||
'name' => '站点地址',
|
||||
'type' => 'input',
|
||||
'placeholder' => '例如:https://dns.example.com',
|
||||
'required' => true,
|
||||
],
|
||||
'uid' => [
|
||||
'name' => '用户 ID',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'key' => [
|
||||
'name' => 'API 密钥',
|
||||
'type' => 'input',
|
||||
'placeholder' => '',
|
||||
'required' => true,
|
||||
],
|
||||
'proxy' => [
|
||||
'name' => '使用代理服务器',
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
'0' => '否',
|
||||
'1' => '是',
|
||||
],
|
||||
'value' => '0'
|
||||
],
|
||||
],
|
||||
'remark' => 2,
|
||||
'status' => true,
|
||||
'redirect' => true,
|
||||
'log' => false,
|
||||
'weight' => true,
|
||||
'page' => false,
|
||||
'add' => false,
|
||||
],
|
||||
];
|
||||
|
||||
public static $line_name = [
|
||||
@@ -627,6 +703,7 @@ class DnsHelper
|
||||
'spaceship' => ['DEF' => 'default'],
|
||||
'aliyunesa' => ['DEF' => '0'],
|
||||
'tencenteo' => ['DEF' => 'Default'],
|
||||
'cccyun' => ['DEF' => 'default'],
|
||||
];
|
||||
|
||||
public static function getList()
|
||||
|
||||
@@ -54,6 +54,8 @@ class aliyun implements DeployInterface
|
||||
$this->deploy_oss($cert_id, $config);
|
||||
} elseif ($config['product'] == 'waf') {
|
||||
$this->deploy_waf($cert_id, $config);
|
||||
} elseif ($config['product'] == 'wafres') {
|
||||
$this->deploy_waf_res($cert_id, $config);
|
||||
} elseif ($config['product'] == 'waf2') {
|
||||
$this->deploy_waf2($cert_id, $config);
|
||||
} elseif ($config['product'] == 'ddoscoo') {
|
||||
@@ -157,9 +159,9 @@ class aliyun implements DeployInterface
|
||||
if (empty($config['domain'])) throw new Exception('DCDN绑定域名不能为空');
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, 'dcdn.aliyuncs.com', '2018-01-15', $this->proxy);
|
||||
foreach (explode(',', $config['domain']) as $domain) {
|
||||
$param = [
|
||||
'Action' => 'SetDcdnDomainSSLCertificate',
|
||||
'DomainName' => $domain,
|
||||
$param = [
|
||||
'Action' => 'SetDcdnDomainSSLCertificate',
|
||||
'DomainName' => $domain,
|
||||
'CertName' => $cert_name,
|
||||
'CertType' => 'cas',
|
||||
'SSLProtocol' => 'on',
|
||||
@@ -439,6 +441,119 @@ class aliyun implements DeployInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_waf_res($cert_id, $config)
|
||||
{
|
||||
if (empty($config['waf_resource_id'])) throw new Exception('云产品防护对象ID不能为空');
|
||||
$deploy_type = isset($config['deploy_type']) ? intval($config['deploy_type']) : 0;
|
||||
|
||||
if ($config['region'] == 'ap-southeast-1') {
|
||||
$cert_id .= '-ap-southeast-1';
|
||||
} else {
|
||||
$cert_id .= '-cn-hangzhou';
|
||||
}
|
||||
|
||||
$endpoint = 'wafopenapi.' . $config['region'] . '.aliyuncs.com';
|
||||
|
||||
$client = new AliyunClient($this->AccessKeyId, $this->AccessKeySecret, $endpoint, '2021-10-01', $this->proxy);
|
||||
|
||||
$param = [
|
||||
'Action' => 'DescribeInstance',
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('获取WAF实例详情失败:' . $e->getMessage());
|
||||
}
|
||||
if (empty($data['InstanceId'])) throw new Exception('当前账号未找到WAF实例');
|
||||
$instance_id = $data['InstanceId'];
|
||||
$this->log('获取WAF实例ID成功 InstanceId=' . $instance_id);
|
||||
|
||||
foreach (explode(',', $config['waf_resource_id']) as $waf_resource_id) {
|
||||
$parts = explode('-', $waf_resource_id);
|
||||
$resource_instance_id = $parts[count($parts) - 3] ?? '';
|
||||
if (empty($resource_instance_id)) {
|
||||
throw new Exception('ResourceInstanceId解析失败:' . $waf_resource_id);
|
||||
}
|
||||
$param = [
|
||||
'Action' => 'DescribeCloudResourceList',
|
||||
'InstanceId' => $instance_id,
|
||||
'CloudResourceId' => $waf_resource_id,
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('查询云产品接入WAF配置失败:' . $e->getMessage());
|
||||
}
|
||||
if (empty($data['CloudResourceList'])) {
|
||||
throw new Exception('WAF云产品接入实例不存在:' . $waf_resource_id);
|
||||
}
|
||||
|
||||
if ($deploy_type == 0) {
|
||||
$param = [
|
||||
'Action' => 'ModifyCloudResourceDefaultCert',
|
||||
'InstanceId' => $instance_id,
|
||||
'CloudResourceId' => $waf_resource_id,
|
||||
'CertId' => $cert_id,
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('WAF云产品防护对象 ' . $waf_resource_id . ' 部署默认证书成功!');
|
||||
} else {
|
||||
$param = [
|
||||
'Action' => 'CreateCloudResourceExtensionCert',
|
||||
'InstanceId' => $instance_id,
|
||||
'CloudResourceId' => $waf_resource_id,
|
||||
'CertId' => $cert_id,
|
||||
'RegionId' => $config['region'],
|
||||
];
|
||||
$client->request($param);
|
||||
$this->log('WAF云产品防护对象 ' . $waf_resource_id . ' 部署扩展证书成功!');
|
||||
|
||||
$this->clean_waf_res_expired_certs($client, $instance_id, $resource_instance_id, $waf_resource_id, $config['region']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function clean_waf_res_expired_certs($client, $instance_id, $resource_instance_id, $waf_resource_id, $region)
|
||||
{
|
||||
$param = [
|
||||
'Action' => 'DescribeResourceInstanceCerts',
|
||||
'InstanceId' => $instance_id,
|
||||
'ResourceInstanceId' => $resource_instance_id,
|
||||
'RegionId' => $region,
|
||||
];
|
||||
try {
|
||||
$data = $client->request($param, 'GET');
|
||||
} catch (Exception $e) {
|
||||
$this->log('查询扩展证书列表失败:' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
if (empty($data['Certs'])) return;
|
||||
|
||||
$now = time();
|
||||
foreach ($data['Certs'] as $cert) {
|
||||
if (empty($cert['CertIdentifier']) || empty($cert['AfterDate'])) continue;
|
||||
$expire_time = strtotime($cert['AfterDate']);
|
||||
if ($expire_time !== false && $expire_time < $now) {
|
||||
$param = [
|
||||
'Action' => 'DeleteCloudResourceExtensionCert',
|
||||
'InstanceId' => $instance_id,
|
||||
'CloudResourceId' => $waf_resource_id,
|
||||
'CertId' => $cert['CertIdentifier'],
|
||||
'RegionId' => $region,
|
||||
];
|
||||
try {
|
||||
$client->request($param);
|
||||
$this->log('已删除过期扩展证书:' . $cert['CertIdentifier']);
|
||||
} catch (Exception $e) {
|
||||
$this->log('删除过期扩展证书失败:' . $cert['CertIdentifier'] . ' ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function deploy_waf2($cert_id, $config)
|
||||
{
|
||||
if (empty($config['domain'])) throw new Exception('WAF绑定域名不能为空');
|
||||
|
||||
375
app/lib/deploy/nginxproxymanager.php
Normal file
375
app/lib/deploy/nginxproxymanager.php
Normal file
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\deploy;
|
||||
|
||||
use app\lib\DeployInterface;
|
||||
use Exception;
|
||||
|
||||
class nginxproxymanager implements DeployInterface
|
||||
{
|
||||
private $logger;
|
||||
private $url;
|
||||
private $email;
|
||||
private $password;
|
||||
private $proxy;
|
||||
private $token;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->url = rtrim($config['url'] ?? '', '/');
|
||||
$this->email = trim($config['email'] ?? '');
|
||||
$this->password = $config['password'] ?? '';
|
||||
$this->proxy = isset($config['proxy']) && $config['proxy'] == 1;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if (empty($this->url) || empty($this->email) || empty($this->password)) {
|
||||
throw new Exception('请填写面板地址、登录邮箱和登录密码');
|
||||
}
|
||||
|
||||
$this->login();
|
||||
$this->request('GET', '/nginx/certificates');
|
||||
}
|
||||
|
||||
public function deploy($fullchain, $privatekey, $config, &$info)
|
||||
{
|
||||
$domains = $config['domainList'] ?? [];
|
||||
$domains = array_values(array_filter(array_map('trim', $domains)));
|
||||
if (empty($domains)) {
|
||||
throw new Exception('没有设置要部署的域名');
|
||||
}
|
||||
|
||||
$this->login();
|
||||
|
||||
$certificateId = intval($config['id'] ?? 0);
|
||||
if ($certificateId > 0) {
|
||||
$this->log('使用配置中的证书ID:' . $certificateId . ' 直接更新 NPM 自定义证书');
|
||||
$certificate = $this->getCertificate($certificateId);
|
||||
$this->assertCustomCertificate($certificate, $certificateId);
|
||||
$this->uploadCertificate($certificateId, $fullchain, $privatekey);
|
||||
$this->log('证书ID:' . $certificateId . ' 更新成功!');
|
||||
return;
|
||||
}
|
||||
|
||||
$hostId = intval($config['host_id'] ?? 0);
|
||||
$hosts = $this->resolveTargetHosts($domains, $hostId);
|
||||
if (empty($hosts)) {
|
||||
throw new Exception('未找到匹配的 Proxy Host,请填写证书ID或 Proxy Host ID');
|
||||
}
|
||||
|
||||
$this->log('匹配到 Proxy Host ' . count($hosts) . ' 个');
|
||||
|
||||
$resolvedCertificateId = 0;
|
||||
$conflictMessage = null;
|
||||
foreach ($hosts as $host) {
|
||||
$hostCertificateId = intval($host['certificate_id'] ?? 0);
|
||||
if ($hostCertificateId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$certificate = $this->getCertificate($hostCertificateId);
|
||||
$this->assertCustomCertificate($certificate, $hostCertificateId);
|
||||
|
||||
if ($resolvedCertificateId === 0) {
|
||||
$resolvedCertificateId = $hostCertificateId;
|
||||
} elseif ($resolvedCertificateId !== $hostCertificateId) {
|
||||
$conflictMessage = '匹配到多个 Proxy Host,但它们绑定了不同的自定义证书ID,无法自动决定更新哪个证书,请手动填写证书ID';
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->log('Proxy Host ID:' . $host['id'] . ' 当前证书不可直接更新:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($conflictMessage !== null) {
|
||||
throw new Exception($conflictMessage);
|
||||
}
|
||||
|
||||
if ($resolvedCertificateId === 0) {
|
||||
$resolvedCertificateId = $this->createCustomCertificate($domains);
|
||||
$this->log('创建自定义证书成功,证书ID:' . $resolvedCertificateId);
|
||||
}
|
||||
|
||||
$this->uploadCertificate($resolvedCertificateId, $fullchain, $privatekey);
|
||||
$this->log('证书ID:' . $resolvedCertificateId . ' 更新成功!');
|
||||
|
||||
foreach ($hosts as $host) {
|
||||
$currentCertificateId = intval($host['certificate_id'] ?? 0);
|
||||
if ($currentCertificateId !== $resolvedCertificateId) {
|
||||
$this->updateProxyHostCertificate($host, $resolvedCertificateId);
|
||||
$this->log('Proxy Host ID:' . $host['id'] . ' 已绑定到证书ID:' . $resolvedCertificateId);
|
||||
} else {
|
||||
$this->log('Proxy Host ID:' . $host['id'] . ' 已绑定目标证书,无需重复更新绑定');
|
||||
}
|
||||
}
|
||||
|
||||
$info['config']['id'] = (string)$resolvedCertificateId;
|
||||
}
|
||||
|
||||
public function setLogger($func)
|
||||
{
|
||||
$this->logger = $func;
|
||||
}
|
||||
|
||||
private function log($txt)
|
||||
{
|
||||
if ($this->logger) {
|
||||
call_user_func($this->logger, $txt);
|
||||
}
|
||||
}
|
||||
|
||||
private function login()
|
||||
{
|
||||
$data = $this->request('POST', '/tokens', [
|
||||
'identity' => $this->email,
|
||||
'secret' => $this->password,
|
||||
], false, false);
|
||||
|
||||
if (empty($data['token'])) {
|
||||
if (!empty($data['requires_2fa'])) {
|
||||
throw new Exception('当前 NPM 账户启用了双因素认证,暂不支持');
|
||||
}
|
||||
throw new Exception('登录 NPM 失败,未返回访问令牌');
|
||||
}
|
||||
|
||||
$this->token = $data['token'];
|
||||
}
|
||||
|
||||
private function resolveTargetHosts(array $domains, int $hostId): array
|
||||
{
|
||||
if ($hostId > 0) {
|
||||
return [$this->getProxyHost($hostId)];
|
||||
}
|
||||
|
||||
$hosts = $this->request('GET', '/nginx/proxy-hosts');
|
||||
if (!is_array($hosts)) {
|
||||
throw new Exception('获取 Proxy Host 列表失败');
|
||||
}
|
||||
|
||||
$matched = [];
|
||||
foreach ($hosts as $host) {
|
||||
$hostDomains = $host['domain_names'] ?? [];
|
||||
if ($this->hasIntersectDomain($domains, $hostDomains)) {
|
||||
$matched[] = $this->getProxyHost(intval($host['id']));
|
||||
}
|
||||
}
|
||||
|
||||
return $matched;
|
||||
}
|
||||
|
||||
private function hasIntersectDomain(array $domains, array $hostDomains): bool
|
||||
{
|
||||
foreach ($hostDomains as $hostDomain) {
|
||||
$hostDomain = trim((string)$hostDomain);
|
||||
if ($hostDomain === '') {
|
||||
continue;
|
||||
}
|
||||
foreach ($domains as $domain) {
|
||||
if ($this->domainMatches($domain, $hostDomain) || $this->domainMatches($hostDomain, $domain)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function domainMatches(string $pattern, string $domain): bool
|
||||
{
|
||||
$pattern = strtolower(trim($pattern));
|
||||
$domain = strtolower(trim($domain));
|
||||
if ($pattern === '' || $domain === '') {
|
||||
return false;
|
||||
}
|
||||
if ($pattern === $domain) {
|
||||
return true;
|
||||
}
|
||||
if (str_starts_with($pattern, '*.')) {
|
||||
$suffix = substr($pattern, 1);
|
||||
return str_ends_with($domain, $suffix);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function createCustomCertificate(array $domains): int
|
||||
{
|
||||
$result = $this->request('POST', '/nginx/certificates', [
|
||||
'provider' => 'other',
|
||||
'nice_name' => $this->buildCertificateName($domains),
|
||||
]);
|
||||
|
||||
if (isset($result['owner_user_id'])) {
|
||||
$this->log('NPM 新建证书归属用户ID:' . intval($result['owner_user_id']) . '(由当前登录账号决定)');
|
||||
}
|
||||
|
||||
$certificateId = intval($result['id'] ?? 0);
|
||||
if ($certificateId <= 0) {
|
||||
throw new Exception('创建 NPM 自定义证书失败');
|
||||
}
|
||||
return $certificateId;
|
||||
}
|
||||
|
||||
private function buildCertificateName(array $domains): string
|
||||
{
|
||||
return trim($domains[0]);
|
||||
}
|
||||
|
||||
private function uploadCertificate(int $certificateId, string $fullchain, string $privatekey): void
|
||||
{
|
||||
[$certificate, $intermediateCertificate] = $this->splitFullchain($fullchain);
|
||||
|
||||
$multipart = [
|
||||
[
|
||||
'name' => 'certificate',
|
||||
'filename' => 'certificate.pem',
|
||||
'contents' => $certificate,
|
||||
],
|
||||
[
|
||||
'name' => 'certificate_key',
|
||||
'filename' => 'certificate.key',
|
||||
'contents' => $privatekey,
|
||||
],
|
||||
];
|
||||
|
||||
if ($intermediateCertificate !== '') {
|
||||
$multipart[] = [
|
||||
'name' => 'intermediate_certificate',
|
||||
'filename' => 'intermediate.pem',
|
||||
'contents' => $intermediateCertificate,
|
||||
];
|
||||
}
|
||||
|
||||
$this->request(
|
||||
'POST',
|
||||
'/nginx/certificates/' . $certificateId . '/upload',
|
||||
$multipart,
|
||||
true,
|
||||
true,
|
||||
['Content-Type' => 'multipart/form-data']
|
||||
);
|
||||
}
|
||||
|
||||
private function splitFullchain(string $fullchain): array
|
||||
{
|
||||
preg_match_all('/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/s', $fullchain, $matches);
|
||||
$certificates = array_values(array_filter(array_map('trim', $matches[0] ?? [])));
|
||||
if (empty($certificates)) {
|
||||
throw new Exception('证书内容格式错误,未找到 PEM 证书块');
|
||||
}
|
||||
|
||||
$certificate = $certificates[0] . "\n";
|
||||
$intermediateCertificate = '';
|
||||
if (count($certificates) > 1) {
|
||||
$intermediateCertificate = implode("\n", array_slice($certificates, 1)) . "\n";
|
||||
}
|
||||
|
||||
return [$certificate, $intermediateCertificate];
|
||||
}
|
||||
|
||||
private function updateProxyHostCertificate(array $host, int $certificateId): void
|
||||
{
|
||||
$payload = [
|
||||
'certificate_id' => $certificateId,
|
||||
];
|
||||
|
||||
$this->request('PUT', '/nginx/proxy-hosts/' . intval($host['id']), $payload);
|
||||
}
|
||||
|
||||
private function assertCustomCertificate(array $certificate, int $certificateId): void
|
||||
{
|
||||
if (($certificate['provider'] ?? '') !== 'other') {
|
||||
throw new Exception('证书ID:' . $certificateId . ' 不是自定义证书(provider=other),无法通过上传接口更新');
|
||||
}
|
||||
}
|
||||
|
||||
private function getCertificate(int $certificateId): array
|
||||
{
|
||||
$certificate = $this->request('GET', '/nginx/certificates/' . $certificateId);
|
||||
if (!is_array($certificate) || empty($certificate['id'])) {
|
||||
throw new Exception('证书ID:' . $certificateId . ' 不存在');
|
||||
}
|
||||
return $certificate;
|
||||
}
|
||||
|
||||
private function getProxyHost(int $hostId): array
|
||||
{
|
||||
$host = $this->request('GET', '/nginx/proxy-hosts/' . $hostId);
|
||||
if (!is_array($host) || empty($host['id'])) {
|
||||
throw new Exception('Proxy Host ID:' . $hostId . ' 不存在');
|
||||
}
|
||||
|
||||
$this->log('读取 Proxy Host ID:' . intval($host['id']) . ' owner_user_id:' . intval($host['owner_user_id'] ?? 0) . ' certificate_id:' . intval($host['certificate_id'] ?? 0));
|
||||
|
||||
return $host;
|
||||
}
|
||||
|
||||
private function request(string $method, string $path, $params = null, bool $auth = true, bool $logBodyOnError = true, array $extraHeaders = [])
|
||||
{
|
||||
$headers = $extraHeaders;
|
||||
if (!isset($headers['Content-Type']) && $params !== null && strtoupper($method) !== 'GET') {
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
if ($auth) {
|
||||
if (empty($this->token)) {
|
||||
throw new Exception('NPM 访问令牌不存在,请先登录');
|
||||
}
|
||||
$headers['Authorization'] = 'Bearer ' . $this->token;
|
||||
}
|
||||
|
||||
$requestData = $params;
|
||||
if ($params !== null && isset($headers['Content-Type']) && strtolower($headers['Content-Type']) !== 'multipart/form-data') {
|
||||
$requestData = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
$response = http_request(
|
||||
$this->url . '/api' . $path,
|
||||
$requestData,
|
||||
null,
|
||||
null,
|
||||
$headers,
|
||||
$this->proxy,
|
||||
$method,
|
||||
30
|
||||
);
|
||||
|
||||
$body = $response['body'] ?? '';
|
||||
$result = json_decode($body, true);
|
||||
if ($response['code'] >= 200 && $response['code'] < 300) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($logBodyOnError && $body !== '') {
|
||||
$this->log('Response:' . $body);
|
||||
}
|
||||
|
||||
if (isset($result['error']['message'])) {
|
||||
throw new Exception($result['error']['message']);
|
||||
}
|
||||
if (isset($result['message'])) {
|
||||
throw new Exception($result['message']);
|
||||
}
|
||||
if (isset($result['error']) && is_string($result['error']) && $result['error'] !== '') {
|
||||
throw new Exception($result['error']);
|
||||
}
|
||||
if ($body !== '') {
|
||||
throw new Exception('请求失败(httpCode=' . $response['code'] . '): ' . $this->truncateResponseBody($body));
|
||||
}
|
||||
|
||||
throw new Exception('请求失败(httpCode=' . $response['code'] . ')');
|
||||
}
|
||||
|
||||
private function truncateResponseBody(string $body): string
|
||||
{
|
||||
$body = trim($body);
|
||||
if ($body === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (mb_strlen($body) > 300) {
|
||||
return mb_substr($body, 0, 300) . '...';
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
282
app/lib/dns/dnsmgr.php
Normal file
282
app/lib/dns/dnsmgr.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\dns;
|
||||
|
||||
use app\lib\DnsInterface;
|
||||
use Exception;
|
||||
|
||||
class dnsmgr implements DnsInterface
|
||||
{
|
||||
private $uid;
|
||||
private $key;
|
||||
private $baseUrl;
|
||||
private $error;
|
||||
private $domain;
|
||||
private $domainid;
|
||||
private $proxy;
|
||||
private $domainInfo;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->uid = $config['uid'];
|
||||
$this->key = $config['key'];
|
||||
$this->baseUrl = rtrim($config['base_url'], '/');
|
||||
$proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->proxy = $proxy;
|
||||
$this->domain = $config['domain'];
|
||||
$this->domainid = $config['domainid'];
|
||||
}
|
||||
|
||||
public function getError()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if ($this->getDomainList() != false) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
|
||||
{
|
||||
$offset = ($PageNumber - 1) * $PageSize;
|
||||
$param = [
|
||||
'offset' => $offset,
|
||||
'limit' => $PageSize,
|
||||
];
|
||||
if (!isNullOrEmpty($KeyWord)) {
|
||||
$param['kw'] = $KeyWord;
|
||||
}
|
||||
|
||||
$data = $this->send_request('/api/domain', $param);
|
||||
if ($data && isset($data['rows'])) {
|
||||
$list = [];
|
||||
foreach ($data['rows'] as $row) {
|
||||
$list[] = [
|
||||
'DomainId' => $row['id'],
|
||||
'Domain' => $row['name'],
|
||||
'RecordCount' => $row['recordcount'],
|
||||
];
|
||||
}
|
||||
return ['total' => $data['total'], 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
|
||||
{
|
||||
$offset = ($PageNumber - 1) * $PageSize;
|
||||
$param = [
|
||||
'offset' => $offset,
|
||||
'limit' => $PageSize,
|
||||
];
|
||||
if (!isNullOrEmpty($KeyWord)) $param['keyword'] = $KeyWord;
|
||||
if (!isNullOrEmpty($SubDomain)) $param['subdomain'] = $SubDomain;
|
||||
if (!isNullOrEmpty($Value)) $param['value'] = $Value;
|
||||
if (!isNullOrEmpty($Type)) $param['type'] = $Type;
|
||||
if (!isNullOrEmpty($Line)) $param['line'] = $Line;
|
||||
if (!isNullOrEmpty($Status)) $param['status'] = $Status;
|
||||
|
||||
$data = $this->send_request('/api/record/data/' . $this->domainid, $param);
|
||||
if ($data && isset($data['rows'])) {
|
||||
$list = [];
|
||||
foreach ($data['rows'] as $row) {
|
||||
$list[] = [
|
||||
'RecordId' => $row['RecordId'],
|
||||
'Domain' => $row['Domain'],
|
||||
'Name' => $row['Name'],
|
||||
'Type' => $row['Type'],
|
||||
'Value' => $row['Value'],
|
||||
'Line' => $row['Line'],
|
||||
'LineName' => $row['LineName'],
|
||||
'TTL' => $row['TTL'],
|
||||
'MX' => $row['MX'],
|
||||
'Status' => $row['Status'],
|
||||
'Weight' => $row['Weight'],
|
||||
'Remark' => $row['Remark'],
|
||||
'UpdateTime' => $row['UpdateTime'],
|
||||
];
|
||||
}
|
||||
return ['total' => $data['total'], 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
|
||||
{
|
||||
if ($SubDomain == '') $SubDomain = '@';
|
||||
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
|
||||
}
|
||||
|
||||
public function getDomainRecordInfo($RecordId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$param = [
|
||||
'name' => $Name,
|
||||
'type' => $Type,
|
||||
'value' => $Value,
|
||||
'line' => $Line,
|
||||
'ttl' => intval($TTL),
|
||||
];
|
||||
if ($Type == 'MX' && !isNullOrEmpty($MX)) {
|
||||
$param['mx'] = intval($MX);
|
||||
}
|
||||
if (!isNullOrEmpty($Weight)) {
|
||||
$param['weight'] = intval($Weight);
|
||||
}
|
||||
if (!isNullOrEmpty($Remark)) {
|
||||
$param['remark'] = $Remark;
|
||||
}
|
||||
|
||||
$data = $this->send_request('/api/record/add/' . $this->domainid, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$param = [
|
||||
'recordid' => $RecordId,
|
||||
'name' => $Name,
|
||||
'type' => $Type,
|
||||
'value' => $Value,
|
||||
'line' => $Line,
|
||||
'ttl' => intval($TTL),
|
||||
];
|
||||
if ($Type == 'MX' && !isNullOrEmpty($MX)) {
|
||||
$param['mx'] = intval($MX);
|
||||
}
|
||||
if (!isNullOrEmpty($Weight)) {
|
||||
$param['weight'] = intval($Weight);
|
||||
}
|
||||
if (!isNullOrEmpty($Remark)) {
|
||||
$param['remark'] = $Remark;
|
||||
}
|
||||
|
||||
$data = $this->send_request('/api/record/update/' . $this->domainid, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
public function updateDomainRecordRemark($RecordId, $Remark)
|
||||
{
|
||||
$param = [
|
||||
'recordid' => $RecordId,
|
||||
'remark' => $Remark,
|
||||
];
|
||||
|
||||
$data = $this->send_request('/api/record/remark/' . $this->domainid, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
public function deleteDomainRecord($RecordId)
|
||||
{
|
||||
$param = [
|
||||
'recordid' => $RecordId,
|
||||
];
|
||||
|
||||
$data = $this->send_request('/api/record/delete/' . $this->domainid, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
public function setDomainRecordStatus($RecordId, $Status)
|
||||
{
|
||||
$param = [
|
||||
'recordid' => $RecordId,
|
||||
'status' => $Status,
|
||||
];
|
||||
|
||||
$data = $this->send_request('/api/record/status/' . $this->domainid, $param);
|
||||
return $data !== false;
|
||||
}
|
||||
|
||||
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRecordLine()
|
||||
{
|
||||
$data = $this->getDomainInfo();
|
||||
if ($data && isset($data['recordLine'])) {
|
||||
$list = [];
|
||||
foreach ($data['recordLine'] as $row) {
|
||||
$list[$row['id']] = [
|
||||
'name' => $row['name'],
|
||||
'parent' => isset($row['parent']) ? $row['parent'] : null,
|
||||
];
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getMinTTL()
|
||||
{
|
||||
$data = $this->getDomainInfo();
|
||||
if ($data && isset($data['minTTL'])) {
|
||||
return $data['minTTL'];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDomainInfo()
|
||||
{
|
||||
if (!empty($this->domainInfo)) return $this->domainInfo;
|
||||
$data = $this->send_request('/api/domain/' . $this->domainid, ['loginurl' => 0]);
|
||||
if ($data) {
|
||||
$this->domainInfo = $data;
|
||||
return $data;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function addDomain($Domain)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function send_request($path, $param = [])
|
||||
{
|
||||
try {
|
||||
$timestamp = (string)time();
|
||||
$signStr = $this->uid . $timestamp . $this->key;
|
||||
$sign = md5($signStr);
|
||||
|
||||
$url = $this->baseUrl . $path;
|
||||
|
||||
$param['uid'] = $this->uid;
|
||||
$param['timestamp'] = $timestamp;
|
||||
$param['sign'] = $sign;
|
||||
$postData = http_build_query($param);
|
||||
|
||||
$response = http_request($url, $postData, null, null, null, $this->proxy);
|
||||
|
||||
$result = json_decode($response['body'], true);
|
||||
if (isset($result['code']) && $result['code'] == 0) {
|
||||
return isset($result['data']) ? $result['data'] : null;
|
||||
} elseif (isset($result['rows']) && isset($result['total'])) {
|
||||
return $result;
|
||||
} elseif (isset($result['msg'])) {
|
||||
$this->setError($result['msg']);
|
||||
return false;
|
||||
} else {
|
||||
$this->setError($response['body']);
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function setError($message)
|
||||
{
|
||||
$this->error = $message;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ class huawei implements DnsInterface
|
||||
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
|
||||
{
|
||||
$offset = ($PageNumber - 1) * $PageSize;
|
||||
$query = ['type' => $Type, 'line_id' => $Line, 'name' => $KeyWord, 'offset' => $offset, 'limit' => $PageSize];
|
||||
$query = ['type' => $Type, 'line_id' => $Line, 'name' => $KeyWord, 'offset' => $offset, 'limit' => $PageSize, 'records' => $Value];
|
||||
if (!isNullOrEmpty($Status)) {
|
||||
$Status = $Status == '1' ? 'ACTIVE' : 'DISABLE';
|
||||
$query['status'] = $Status;
|
||||
|
||||
499
app/lib/dns/technitium.php
Normal file
499
app/lib/dns/technitium.php
Normal file
@@ -0,0 +1,499 @@
|
||||
<?php
|
||||
|
||||
namespace app\lib\dns;
|
||||
|
||||
use app\lib\DnsInterface;
|
||||
use Exception;
|
||||
|
||||
class technitium implements DnsInterface
|
||||
{
|
||||
private $url;
|
||||
private $token;
|
||||
private $error;
|
||||
private $domain;
|
||||
private $domainid;
|
||||
private $proxy;
|
||||
|
||||
function __construct($config)
|
||||
{
|
||||
$this->url = rtrim($config['url'], '/') . '/api';
|
||||
$this->token = $config['token'];
|
||||
$this->proxy = isset($config['proxy']) ? $config['proxy'] == 1 : false;
|
||||
$this->domain = $config['domain'];
|
||||
$this->domainid = $config['domainid'];
|
||||
}
|
||||
|
||||
public function getError()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
if ($this->getDomainList() !== false) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDomainList($KeyWord = null, $PageNumber = 1, $PageSize = 20)
|
||||
{
|
||||
$data = $this->send_request('GET', '/zones/list');
|
||||
if ($data && isset($data['response']['zones'])) {
|
||||
$list = [];
|
||||
foreach ($data['response']['zones'] as $zone) {
|
||||
$list[] = [
|
||||
'DomainId' => $zone['name'],
|
||||
'Domain' => $zone['name'],
|
||||
'RecordCount' => 0,
|
||||
];
|
||||
}
|
||||
if (!isNullOrEmpty($KeyWord)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($KeyWord) {
|
||||
return strpos($v['Domain'], $KeyWord) !== false;
|
||||
}));
|
||||
}
|
||||
return ['total' => count($list), 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDomainRecords($PageNumber = 1, $PageSize = 20, $KeyWord = null, $SubDomain = null, $Value = null, $Type = null, $Line = null, $Status = null)
|
||||
{
|
||||
$params = ['domain' => $this->domain, 'listZone' => 'true'];
|
||||
$data = $this->send_request('GET', '/zones/records/get', $params);
|
||||
if ($data && isset($data['response']['records'])) {
|
||||
$list = [];
|
||||
$records = $data['response']['records'];
|
||||
foreach ($records as $i => &$row) {
|
||||
$row['id'] = $i;
|
||||
$name = $row['name'] == $this->domain ? '@' : str_replace('.' . $this->domain, '', $row['name']);
|
||||
$value = '';
|
||||
$mx = null;
|
||||
$rData = $row['rData'];
|
||||
|
||||
if ($row['type'] == 'A' || $row['type'] == 'AAAA') {
|
||||
$value = isset($rData['ipAddress']) ? $rData['ipAddress'] : '';
|
||||
} elseif ($row['type'] == 'CNAME') {
|
||||
$value = isset($rData['cname']) ? $rData['cname'] : '';
|
||||
} elseif ($row['type'] == 'NS') {
|
||||
$value = isset($rData['nameServer']) ? $rData['nameServer'] : '';
|
||||
} elseif ($row['type'] == 'MX') {
|
||||
$value = isset($rData['exchange']) ? $rData['exchange'] : '';
|
||||
$mx = isset($rData['preference']) ? $rData['preference'] : 1;
|
||||
} elseif ($row['type'] == 'TXT') {
|
||||
$value = isset($rData['text']) ? $rData['text'] : '';
|
||||
} elseif ($row['type'] == 'SRV') {
|
||||
$value = (isset($rData['priority']) ? $rData['priority'] : 0) . ' ' . (isset($rData['weight']) ? $rData['weight'] : 0) . ' ' . (isset($rData['port']) ? $rData['port'] : 0) . ' ' . (isset($rData['target']) ? $rData['target'] : '');
|
||||
} elseif ($row['type'] == 'PTR') {
|
||||
$value = isset($rData['ptrName']) ? $rData['ptrName'] : '';
|
||||
} elseif ($row['type'] == 'CAA') {
|
||||
$value = (isset($rData['flags']) ? $rData['flags'] : 0) . ' ' . (isset($rData['tag']) ? $rData['tag'] : '') . ' "' . (isset($rData['value']) ? $rData['value'] : '') . '"';
|
||||
} elseif ($row['type'] == 'ANAME') {
|
||||
$value = isset($rData['aname']) ? $rData['aname'] : '';
|
||||
} elseif ($row['type'] == 'DNAME') {
|
||||
$value = isset($rData['dname']) ? $rData['dname'] : '';
|
||||
} elseif ($row['type'] == 'APP') {
|
||||
$value = (isset($rData['appName']) ? $rData['appName'] : '') . ' ' . (isset($rData['classPath']) ? $rData['classPath'] : '');
|
||||
if (!empty($rData['recordData'])) {
|
||||
$value .= ' ' . $rData['recordData'];
|
||||
}
|
||||
}
|
||||
|
||||
$list[] = [
|
||||
'RecordId' => $i,
|
||||
'Domain' => $this->domain,
|
||||
'Name' => $name,
|
||||
'Type' => $row['type'],
|
||||
'Value' => $value,
|
||||
'Line' => 'default',
|
||||
'TTL' => $row['ttl'],
|
||||
'MX' => $mx,
|
||||
'Status' => $row['disabled'] ? '0' : '1',
|
||||
'Weight' => null,
|
||||
'Remark' => isset($row['comments']) ? $row['comments'] : null,
|
||||
'UpdateTime' => null,
|
||||
];
|
||||
}
|
||||
cache('technitium_' . $this->domain, $records, 86400);
|
||||
|
||||
if (!isNullOrEmpty($SubDomain)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($SubDomain) {
|
||||
return strcasecmp($v['Name'], $SubDomain) === 0;
|
||||
}));
|
||||
} else {
|
||||
if (!isNullOrEmpty($KeyWord)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($KeyWord) {
|
||||
return strpos($v['Name'], $KeyWord) !== false || strpos($v['Value'], $KeyWord) !== false;
|
||||
}));
|
||||
}
|
||||
if (!isNullOrEmpty($Value)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($Value) {
|
||||
return $v['Value'] == $Value;
|
||||
}));
|
||||
}
|
||||
if (!isNullOrEmpty($Type)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($Type) {
|
||||
return $v['Type'] == $Type;
|
||||
}));
|
||||
}
|
||||
if (!isNullOrEmpty($Status)) {
|
||||
$list = array_values(array_filter($list, function ($v) use ($Status) {
|
||||
return $v['Status'] == $Status;
|
||||
}));
|
||||
}
|
||||
}
|
||||
return ['total' => count($list), 'list' => $list];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getSubDomainRecords($SubDomain, $PageNumber = 1, $PageSize = 20, $Type = null, $Line = null)
|
||||
{
|
||||
return $this->getDomainRecords($PageNumber, $PageSize, null, $SubDomain, null, $Type, $Line);
|
||||
}
|
||||
|
||||
public function getDomainRecordInfo($RecordId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function buildRecordParams($Type, $Value, $MX = 1)
|
||||
{
|
||||
$params = [];
|
||||
if ($Type == 'A' || $Type == 'AAAA') {
|
||||
$params['ipAddress'] = $Value;
|
||||
} elseif ($Type == 'CNAME') {
|
||||
$params['cname'] = $Value;
|
||||
} elseif ($Type == 'NS') {
|
||||
$params['nameServer'] = $Value;
|
||||
} elseif ($Type == 'MX') {
|
||||
$params['exchange'] = $Value;
|
||||
$params['preference'] = intval($MX);
|
||||
} elseif ($Type == 'TXT') {
|
||||
$params['text'] = $Value;
|
||||
} elseif ($Type == 'SRV') {
|
||||
$parts = explode(' ', $Value);
|
||||
if (count($parts) == 4) {
|
||||
$params['priority'] = $parts[0];
|
||||
$params['weight'] = $parts[1];
|
||||
$params['port'] = $parts[2];
|
||||
$params['target'] = $parts[3];
|
||||
}
|
||||
} elseif ($Type == 'PTR') {
|
||||
$params['ptrName'] = $Value;
|
||||
} elseif ($Type == 'CAA') {
|
||||
$parts = explode(' ', $Value, 3);
|
||||
if (count($parts) == 3) {
|
||||
$params['flags'] = $parts[0];
|
||||
$params['tag'] = $parts[1];
|
||||
$params['value'] = trim($parts[2], '"');
|
||||
}
|
||||
} elseif ($Type == 'ANAME') {
|
||||
$params['aname'] = $Value;
|
||||
} elseif ($Type == 'DNAME') {
|
||||
$params['dname'] = $Value;
|
||||
} elseif ($Type == 'APP') {
|
||||
$parts = explode(' ', $Value, 3);
|
||||
if (count($parts) >= 2) {
|
||||
$params['appName'] = $parts[0];
|
||||
$params['classPath'] = $parts[1];
|
||||
$params['recordData'] = rtrim(isset($parts[2]) ? $parts[2] : '');
|
||||
} else {
|
||||
$params['appName'] = rtrim($Value);
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
private function getOldValueParams($Type, $rData)
|
||||
{
|
||||
$params = [];
|
||||
if ($Type == 'A' || $Type == 'AAAA') {
|
||||
$params['ipAddress'] = isset($rData['ipAddress']) ? $rData['ipAddress'] : '';
|
||||
} elseif ($Type == 'CNAME') {
|
||||
$params['cname'] = isset($rData['cname']) ? $rData['cname'] : '';
|
||||
} elseif ($Type == 'NS') {
|
||||
$params['nameServer'] = isset($rData['nameServer']) ? $rData['nameServer'] : '';
|
||||
} elseif ($Type == 'MX') {
|
||||
$params['exchange'] = isset($rData['exchange']) ? $rData['exchange'] : '';
|
||||
$params['preference'] = isset($rData['preference']) ? $rData['preference'] : 1;
|
||||
} elseif ($Type == 'TXT') {
|
||||
$params['text'] = isset($rData['text']) ? $rData['text'] : '';
|
||||
} elseif ($Type == 'SRV') {
|
||||
$params['priority'] = isset($rData['priority']) ? $rData['priority'] : 0;
|
||||
$params['weight'] = isset($rData['weight']) ? $rData['weight'] : 0;
|
||||
$params['port'] = isset($rData['port']) ? $rData['port'] : 0;
|
||||
$params['target'] = isset($rData['target']) ? $rData['target'] : '';
|
||||
} elseif ($Type == 'PTR') {
|
||||
$params['ptrName'] = isset($rData['ptrName']) ? $rData['ptrName'] : '';
|
||||
} elseif ($Type == 'CAA') {
|
||||
$params['flags'] = isset($rData['flags']) ? $rData['flags'] : 0;
|
||||
$params['tag'] = isset($rData['tag']) ? $rData['tag'] : '';
|
||||
$params['value'] = isset($rData['value']) ? $rData['value'] : '';
|
||||
} elseif ($Type == 'ANAME') {
|
||||
$params['aname'] = isset($rData['aname']) ? $rData['aname'] : '';
|
||||
} elseif ($Type == 'DNAME') {
|
||||
$params['dname'] = isset($rData['dname']) ? $rData['dname'] : '';
|
||||
} elseif ($Type == 'APP') {
|
||||
$params['appName'] = isset($rData['appName']) ? $rData['appName'] : '';
|
||||
$params['classPath'] = isset($rData['classPath']) ? $rData['classPath'] : '';
|
||||
if (!empty($rData['recordData'])) {
|
||||
$params['recordData'] = $rData['recordData'];
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
private function getNewValueParams($Type, $Value, $MX = 1)
|
||||
{
|
||||
$params = [];
|
||||
if ($Type == 'A' || $Type == 'AAAA') {
|
||||
$params['newIpAddress'] = $Value;
|
||||
} elseif ($Type == 'CNAME') {
|
||||
$params['newCname'] = $Value;
|
||||
} elseif ($Type == 'NS') {
|
||||
$params['newNameServer'] = $Value;
|
||||
} elseif ($Type == 'MX') {
|
||||
$params['newExchange'] = $Value;
|
||||
$params['newPreference'] = intval($MX);
|
||||
} elseif ($Type == 'TXT') {
|
||||
$params['newText'] = $Value;
|
||||
} elseif ($Type == 'SRV') {
|
||||
$parts = explode(' ', $Value);
|
||||
if (count($parts) == 4) {
|
||||
$params['newPriority'] = $parts[0];
|
||||
$params['newWeight'] = $parts[1];
|
||||
$params['newPort'] = $parts[2];
|
||||
$params['newTarget'] = $parts[3];
|
||||
}
|
||||
} elseif ($Type == 'PTR') {
|
||||
$params['newPtrName'] = $Value;
|
||||
} elseif ($Type == 'CAA') {
|
||||
$parts = explode(' ', $Value, 3);
|
||||
if (count($parts) == 3) {
|
||||
$params['newFlags'] = $parts[0];
|
||||
$params['newTag'] = $parts[1];
|
||||
$params['newValue'] = trim($parts[2], '"');
|
||||
}
|
||||
} elseif ($Type == 'ANAME') {
|
||||
$params['newAName'] = $Value;
|
||||
} elseif ($Type == 'DNAME') {
|
||||
$params['newDName'] = $Value;
|
||||
} elseif ($Type == 'APP') {
|
||||
$parts = explode(' ', $Value, 3);
|
||||
if (count($parts) >= 2) {
|
||||
$params['appName'] = $parts[0];
|
||||
$params['classPath'] = $parts[1];
|
||||
$params['recordData'] = rtrim(isset($parts[2]) ? $parts[2] : '');
|
||||
} else {
|
||||
$params['appName'] = rtrim($Value);
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function addDomainRecord($Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$domain = $Name == '@' ? $this->domain : $Name . '.' . $this->domain;
|
||||
$params = [
|
||||
'domain' => $domain,
|
||||
'zone' => $this->domain,
|
||||
'type' => $Type,
|
||||
'ttl' => intval($TTL)
|
||||
];
|
||||
if (!isNullOrEmpty($Remark)) {
|
||||
$params['comments'] = $Remark;
|
||||
}
|
||||
$valParams = $this->buildRecordParams($Type, $Value, $MX);
|
||||
if (empty($valParams) && $Type != 'SOA') {
|
||||
$this->setError('不受支持的记录类型或参数解析失败');
|
||||
return false;
|
||||
}
|
||||
$params = array_merge($params, $valParams);
|
||||
|
||||
$result = $this->send_request('POST', '/zones/records/add', $params);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
public function updateDomainRecord($RecordId, $Name, $Type, $Value, $Line = 'default', $TTL = 600, $MX = 1, $Weight = null, $Remark = null)
|
||||
{
|
||||
$records = cache('technitium_' . $this->domain);
|
||||
if (!$records || !isset($records[$RecordId])) {
|
||||
$this->setError('记录不存在,请刷新页面重试');
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldRecord = $records[$RecordId];
|
||||
$domain = $oldRecord['name'];
|
||||
$newDomain = $Name == '@' ? $this->domain : $Name . '.' . $this->domain;
|
||||
|
||||
if ($oldRecord['type'] == 'APP') {
|
||||
$oldValue = (isset($oldRecord['rData']['appName']) ? $oldRecord['rData']['appName'] : '') . ' ' . (isset($oldRecord['rData']['classPath']) ? $oldRecord['rData']['classPath'] : '');
|
||||
if (!empty($oldRecord['rData']['recordData'])) {
|
||||
$oldValue .= ' ' . $oldRecord['rData']['recordData'];
|
||||
}
|
||||
if ($oldValue != rtrim($Value) || $domain != $newDomain) {
|
||||
$this->deleteDomainRecord($RecordId);
|
||||
return $this->addDomainRecord($Name, $Type, $Value, $Line, $TTL, $MX, $Weight, $Remark);
|
||||
}
|
||||
}
|
||||
|
||||
$params = [
|
||||
'domain' => $domain,
|
||||
'zone' => $this->domain,
|
||||
'type' => $oldRecord['type'],
|
||||
'ttl' => intval($TTL),
|
||||
];
|
||||
|
||||
if ($domain != $newDomain) {
|
||||
$params['newDomain'] = $newDomain;
|
||||
}
|
||||
|
||||
$params['comments'] = empty($Remark) ? "" : $Remark;
|
||||
|
||||
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
|
||||
$newValParams = $this->getNewValueParams($Type, $Value, $MX);
|
||||
|
||||
$params = array_merge($params, $oldValParams, $newValParams);
|
||||
$result = $this->send_request('POST', '/zones/records/update', $params);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
public function updateDomainRecordRemark($RecordId, $Remark)
|
||||
{
|
||||
$records = cache('technitium_' . $this->domain);
|
||||
if (!$records || !isset($records[$RecordId])) {
|
||||
$this->setError('记录不存在,请刷新页面重试');
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldRecord = $records[$RecordId];
|
||||
$domain = $oldRecord['name'];
|
||||
|
||||
$params = [
|
||||
'domain' => $domain,
|
||||
'zone' => $this->domain,
|
||||
'type' => $oldRecord['type'],
|
||||
'comments' => $Remark,
|
||||
];
|
||||
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
|
||||
$params = array_merge($params, $oldValParams);
|
||||
|
||||
$result = $this->send_request('POST', '/zones/records/update', $params);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
public function deleteDomainRecord($RecordId)
|
||||
{
|
||||
$records = cache('technitium_' . $this->domain);
|
||||
if (!$records || !isset($records[$RecordId])) {
|
||||
$this->setError('记录不存在,请刷新页面重试');
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldRecord = $records[$RecordId];
|
||||
$domain = $oldRecord['name'];
|
||||
|
||||
$params = [
|
||||
'domain' => $domain,
|
||||
'zone' => $this->domain,
|
||||
'type' => $oldRecord['type'],
|
||||
];
|
||||
|
||||
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
|
||||
$params = array_merge($params, $oldValParams);
|
||||
|
||||
$result = $this->send_request('POST', '/zones/records/delete', $params);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
public function setDomainRecordStatus($RecordId, $Status)
|
||||
{
|
||||
$records = cache('technitium_' . $this->domain);
|
||||
if (!$records || !isset($records[$RecordId])) {
|
||||
$this->setError('记录不存在,请刷新页面重试');
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldRecord = $records[$RecordId];
|
||||
$domain = $oldRecord['name'];
|
||||
|
||||
$params = [
|
||||
'domain' => $domain,
|
||||
'zone' => $this->domain,
|
||||
'type' => $oldRecord['type'],
|
||||
'disable' => $Status == '0' ? 'true' : 'false',
|
||||
];
|
||||
|
||||
$oldValParams = $this->getOldValueParams($oldRecord['type'], $oldRecord['rData']);
|
||||
$params = array_merge($params, $oldValParams);
|
||||
|
||||
$result = $this->send_request('POST', '/zones/records/update', $params);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
public function getDomainRecordLog($PageNumber = 1, $PageSize = 20, $KeyWord = null, $StartDate = null, $endDate = null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRecordLine()
|
||||
{
|
||||
return ['default' => ['name' => '默认', 'parent' => null]];
|
||||
}
|
||||
|
||||
public function getMinTTL()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function addDomain($Domain)
|
||||
{
|
||||
$params = [
|
||||
'zone' => $Domain,
|
||||
'type' => 'Primary'
|
||||
];
|
||||
$result = $this->send_request('POST', '/zones/create', $params);
|
||||
if ($result && isset($result['response']['domain'])) {
|
||||
return ['id' => $result['response']['domain'], 'name' => $result['response']['domain']];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function send_request($method, $path, $params = [])
|
||||
{
|
||||
$url = $this->url . $path;
|
||||
$params['token'] = $this->token;
|
||||
|
||||
$body = null;
|
||||
if ($method == 'GET' || $method == 'DELETE') {
|
||||
$url .= '?' . http_build_query($params);
|
||||
} else {
|
||||
$body = http_build_query($params);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = http_request($url, $body, null, null, null, $this->proxy, $method);
|
||||
} catch (Exception $e) {
|
||||
$this->setError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
$arr = json_decode($response['body'], true);
|
||||
if (isset($arr['status']) && $arr['status'] == 'ok') {
|
||||
return $arr;
|
||||
} elseif (isset($arr['errorMessage'])) {
|
||||
$this->setError($arr['errorMessage']);
|
||||
return false;
|
||||
} else {
|
||||
$this->setError('API 请求失败');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function setError($message)
|
||||
{
|
||||
$this->error = $message;
|
||||
}
|
||||
}
|
||||
@@ -91,14 +91,17 @@ class CloudflareEnhanceService
|
||||
}
|
||||
}
|
||||
|
||||
public function createCustomHostname(string $zoneId, string $hostname, ?string $customOriginServer = null): array
|
||||
public function createCustomHostname(string $zoneId, string $hostname, ?string $customOriginServer = null, string $sslMethod = 'http', string $minTlsVersion = '1.0'): array
|
||||
{
|
||||
$hostname = $this->normalizeHostname($hostname);
|
||||
$payload = [
|
||||
'hostname' => $hostname,
|
||||
'ssl' => [
|
||||
'method' => 'http',
|
||||
'method' => $sslMethod === 'txt' ? 'txt' : 'http',
|
||||
'type' => 'dv',
|
||||
'settings' => [
|
||||
'min_tls_version' => $minTlsVersion
|
||||
]
|
||||
],
|
||||
];
|
||||
$origin = trim((string)$customOriginServer);
|
||||
@@ -180,6 +183,19 @@ class CloudflareEnhanceService
|
||||
}
|
||||
}
|
||||
|
||||
public function getDcvDelegationUuid(string $zoneId): string
|
||||
{
|
||||
try {
|
||||
$result = $this->requestResult('GET', '/zones/' . $zoneId . '/dcv_delegation/uuid', [], null, true);
|
||||
if ($result === null) {
|
||||
return '';
|
||||
}
|
||||
return trim((string)($result['uuid'] ?? ''));
|
||||
} catch (Exception $e) {
|
||||
$this->throwActionError('获取 DCV 委派 UUID', $e, 'SSL and Certificates:Read');
|
||||
}
|
||||
}
|
||||
|
||||
public function listTunnels(string $accountId): array
|
||||
{
|
||||
$this->assertTunnelSupported();
|
||||
|
||||
@@ -41,7 +41,7 @@ class ExpireNoticeService
|
||||
$count = $this->refreshExpiringDomainList($max_day);
|
||||
if ($count > 0) return;
|
||||
|
||||
if (!empty($days) && (config_get('expire_notice_mail') == '1' || config_get('expire_notice_wxtpl') == '1' || config_get('expire_notice_tgbot') == '1' || config_get('expire_notice_webhook') == '1') && date('H') >= 9) {
|
||||
if (!empty($days) && (config_get('expire_notice_mail') == '1' || config_get('expire_notice_wxtpl') == '1' || config_get('expire_notice_tgbot') == '1' || config_get('expire_notice_webhook') == '1' || config_get('expire_notice_custom_webhook') == '1') && date('H') >= 9) {
|
||||
$this->noticeExpiringDomainList($max_day, $days);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ CREATE TABLE `dnsmgr_config` (
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO `dnsmgr_config` VALUES ('version', '1048');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('version', '1049');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('notice_mail', '0');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('notice_wxtpl', '0');
|
||||
INSERT INTO `dnsmgr_config` VALUES ('mail_smtp', 'smtp.qq.com');
|
||||
@@ -26,6 +26,7 @@ DROP TABLE IF EXISTS `dnsmgr_domain`;
|
||||
CREATE TABLE `dnsmgr_domain` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`aid` int(11) unsigned NOT NULL,
|
||||
`cid` int(11) unsigned NOT NULL DEFAULT '0',
|
||||
`name` varchar(255) NOT NULL,
|
||||
`thirdid` varchar(60) DEFAULT NULL,
|
||||
`addtime` datetime DEFAULT NULL,
|
||||
@@ -40,7 +41,8 @@ CREATE TABLE `dnsmgr_domain` (
|
||||
`noticetime` datetime DEFAULT NULL,
|
||||
`checkstatus` tinyint(1) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `name` (`name`)
|
||||
KEY `name` (`name`),
|
||||
KEY `cid` (`cid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `dnsmgr_user`;
|
||||
@@ -261,4 +263,15 @@ CREATE TABLE `dnsmgr_domain_alias` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `did` (`did`),
|
||||
KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
DROP TABLE IF EXISTS `dnsmgr_domain_category`;
|
||||
CREATE TABLE `dnsmgr_domain_category` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`remark` varchar(100) DEFAULT NULL,
|
||||
`sort` int(11) NOT NULL DEFAULT '0',
|
||||
`addtime` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `sort` (`sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -198,4 +198,18 @@ CREATE TABLE IF NOT EXISTS `dnsmgr_domain_alias` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `did` (`did`),
|
||||
KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `dnsmgr_domain_category` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`remark` varchar(100) DEFAULT NULL,
|
||||
`sort` int(11) NOT NULL DEFAULT '0',
|
||||
`addtime` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `sort` (`sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
ALTER TABLE `dnsmgr_domain`
|
||||
ADD COLUMN `cid` int(11) unsigned NOT NULL DEFAULT '0',
|
||||
ADD KEY `cid` (`cid`);
|
||||
@@ -61,6 +61,9 @@ class MsgNotice
|
||||
$content = str_replace(['<br/>', '<b>', '</b>'], ["\n", '**', '**'], $mail_content);
|
||||
self::send_webhook($mail_title, $content);
|
||||
}
|
||||
if (config_get('notice_custom_webhook') == 1) {
|
||||
self::send_custom_webhook($mail_title, $mail_content);
|
||||
}
|
||||
}
|
||||
|
||||
public static function cert_order_send($id, $result)
|
||||
@@ -141,6 +144,9 @@ class MsgNotice
|
||||
$content = str_replace(['*', '<br/>', '<b>', '</b>'], ['\*', "\n", '**', '**'], $mail_content);
|
||||
self::send_webhook($mail_title, $content);
|
||||
}
|
||||
if (config_get('cert_notice_custom_webhook') == 1 || config_get('cert_notice_custom_webhook') == 2 && !$result) {
|
||||
self::send_custom_webhook($mail_title, $mail_content);
|
||||
}
|
||||
}
|
||||
|
||||
public static function expire_notice_send($day, $list)
|
||||
@@ -169,6 +175,9 @@ class MsgNotice
|
||||
$content = str_replace(['*', '<br/>', '<b>', '</b>'], ['\*', "\n", '**', '**'], $mail_content);
|
||||
self::send_webhook($mail_title, $content);
|
||||
}
|
||||
if (config_get('expire_notice_custom_webhook') == 1) {
|
||||
self::send_custom_webhook($mail_title, $mail_content);
|
||||
}
|
||||
}
|
||||
|
||||
public static function send_mail($to, $sub, $msg)
|
||||
@@ -359,6 +368,85 @@ class MsgNotice
|
||||
}
|
||||
}
|
||||
|
||||
public static function send_custom_webhook($title, $content)
|
||||
{
|
||||
$url = config_get('custom_webhook_url');
|
||||
if (!$url || !parse_url($url)) return false;
|
||||
|
||||
$method = strtoupper(config_get('custom_webhook_method') ?: 'POST');
|
||||
$contentType = config_get('custom_webhook_content_type') ?: 'application/json';
|
||||
$headersRaw = config_get('custom_webhook_headers');
|
||||
$bodyTemplate = config_get('custom_webhook_body') ?: '{"title":"{title}","content":"{content}"}';
|
||||
$contentFormat = config_get('custom_webhook_content_format') ?: 'text';
|
||||
|
||||
if ($contentFormat === 'markdown') {
|
||||
$content = str_replace(['<br/>', '<b>', '</b>'], ["\n", '**', '**'], $content);
|
||||
$content = strip_tags($content);
|
||||
} elseif ($contentFormat === 'text') {
|
||||
$content = str_replace('<br/>', "\n", $content);
|
||||
$content = strip_tags($content);
|
||||
}
|
||||
|
||||
$body = str_replace(['{title}', '{content}'], [$title, $content], $bodyTemplate);
|
||||
|
||||
$headers = [];
|
||||
if (!empty($headersRaw)) {
|
||||
$lines = explode("\n", $headersRaw);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) continue;
|
||||
$pos = strpos($line, ':');
|
||||
if ($pos !== false) {
|
||||
$key = trim(substr($line, 0, $pos));
|
||||
$val = trim(substr($line, $pos + 1));
|
||||
if ($key !== '') $headers[$key] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$options = [
|
||||
'timeout' => 10,
|
||||
'verify' => false,
|
||||
'headers' => $headers,
|
||||
'http_errors' => false,
|
||||
];
|
||||
|
||||
if ($method === 'GET') {
|
||||
$params = [];
|
||||
if ($contentType === 'application/json') {
|
||||
$decoded = json_decode($body, true);
|
||||
if (is_array($decoded)) {
|
||||
$params = $decoded;
|
||||
}
|
||||
} else {
|
||||
parse_str($body, $params);
|
||||
}
|
||||
$connector = strpos($url, '?') !== false ? '&' : '?';
|
||||
$url = $url . $connector . http_build_query($params);
|
||||
} else {
|
||||
$options['headers']['Content-Type'] = $contentType;
|
||||
if ($contentType === 'application/json') {
|
||||
json_decode($body);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$body = json_encode(['title' => $title, 'content' => $content]);
|
||||
}
|
||||
}
|
||||
$options['body'] = $body;
|
||||
}
|
||||
|
||||
try {
|
||||
$client = new \GuzzleHttp\Client();
|
||||
$response = $client->request($method, $url, $options);
|
||||
$statusCode = $response->getStatusCode();
|
||||
if ($statusCode >= 200 && $statusCode < 300) {
|
||||
return true;
|
||||
}
|
||||
return '请求失败,HTTP状态码:' . $statusCode;
|
||||
} catch (\Exception $e) {
|
||||
return '请求失败:' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private static function telegram_curl($url, $post)
|
||||
{
|
||||
$ch = curl_init();
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
<label class="col-sm-3 control-label">群机器人Webhook</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="cert_notice_webhook" default="{:config_get('cert_notice_webhook')}"><option value="0">关闭</option><option value="1">开启</option><option value="2">开启(仅失败时)</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">自定义Webhook</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="cert_notice_custom_webhook" default="{:config_get('cert_notice_custom_webhook')}"><option value="0">关闭</option><option value="1">开启</option><option value="2">开启(仅失败时)</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-9"><input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/></div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -106,13 +106,16 @@
|
||||
{if request()->user['type'] eq 'user'}<li class="{:checkIfActive('index')}">
|
||||
<a href="/"><i class="fa fa-home fa-fw"></i> <span>后台首页</span></a>
|
||||
</li>{/if}
|
||||
<li class="{:checkIfActive('domain,record,record_log,record_batch_add,domain_add,weight,record_batch_add2,record_batch_edit2,expire_notice')}">
|
||||
<li class="{:checkIfActive('domain,record,record_log,record_batch_add,domain_add,weight,record_batch_add2,record_batch_edit2,expire_notice,smartparse')}">
|
||||
<a href="/domain"><i class="fa fa-list-ul fa-fw"></i> <span>域名管理</span></a>
|
||||
</li>
|
||||
{if request()->user['level'] eq 2}
|
||||
<li class="{:checkIfActive('account,account_add')}">
|
||||
<a href="/account"><i class="fa fa-lock fa-fw"></i> <span>域名账户</span></a>
|
||||
</li>
|
||||
<li class="{:checkIfActive('category')}">
|
||||
<a href="/domain/category"><i class="fa fa-folder fa-fw"></i> <span>域名分类</span></a>
|
||||
</li>
|
||||
<li class="treeview {:checkIfActive('overview,task,taskinfo,taskform')}">
|
||||
<a href="javascript:;">
|
||||
<i class="fa fa-heartbeat fa-fw"></i>
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
<label class="col-sm-4 control-label">群机器人Webhook</label>
|
||||
<div class="col-sm-8"><select class="form-control" name="notice_webhook" default="{:config_get('notice_webhook')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">自定义Webhook</label>
|
||||
<div class="col-sm-8"><select class="form-control" name="notice_custom_webhook" default="{:config_get('notice_custom_webhook')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
204
app/view/domain/category.html
Normal file
204
app/view/domain/category.html
Normal file
@@ -0,0 +1,204 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}域名分类管理{/block}
|
||||
{block name="main"}
|
||||
<div class="modal" id="modal-store" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
|
||||
<h4 class="modal-title" id="modal-title">添加分类</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-horizontal" id="form-store">
|
||||
<input type="hidden" name="id"/>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">分类名称</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" name="name" placeholder="输入分类名称" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">排序</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="number" class="form-control" name="sort" value="0" placeholder="数字越小越靠前">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">备注</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" name="remark" placeholder="可选">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" id="store" onclick="save()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 center-block" style="float: none;">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">域名分类管理</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form onsubmit="return searchSubmit()" method="GET" class="form-inline" id="searchToolbar">
|
||||
<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加分类</a>
|
||||
<a href="javascript:searchClear()" class="btn btn-default" title="刷新列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
</form>
|
||||
<table id="listTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{block name="script"}
|
||||
<script src="/static/js/layer/layer.js"></script>
|
||||
<script src="/static/js/bootstrap-table-1.21.4.min.js"></script>
|
||||
<script src="/static/js/bootstrap-table-page-jump-to-1.21.4.min.js"></script>
|
||||
<script src="/static/js/bootstrapValidator.min.js"></script>
|
||||
<script src="/static/js/custom.js?v=1003"></script>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
updateToolbar();
|
||||
let defaultPageSize = getCookie('category_pagesize') ? getCookie('category_pagesize') : 10;
|
||||
const pageNumber = typeof window.$_GET['pageNumber'] != 'undefined' ? parseInt(window.$_GET['pageNumber']) : 1;
|
||||
const pageSize = typeof window.$_GET['pageSize'] != 'undefined' ? parseInt(window.$_GET['pageSize']) : defaultPageSize;
|
||||
|
||||
$("#listTable").bootstrapTable({
|
||||
url: '/domain/category/data',
|
||||
pageNumber: pageNumber,
|
||||
pageSize: pageSize,
|
||||
classes: 'table table-striped table-hover table-bordered',
|
||||
uniqueId: 'id',
|
||||
columns: [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID'
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '分类名称'
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
formatter: function(value, row, index) {
|
||||
return value ? value : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '排序'
|
||||
},
|
||||
{
|
||||
field: 'domain_count',
|
||||
title: '域名数量',
|
||||
formatter: function(value, row, index) {
|
||||
return '<span class="label label-info">' + value + '</span>';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'addtime',
|
||||
title: '添加时间'
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
formatter: function(value, row, index) {
|
||||
var html = '<a href="javascript:editframe(\''+row.id+'\')" class="btn btn-primary btn-xs">修改</a> ';
|
||||
html += '<a href="javascript:delItem(\''+row.id+'\')" class="btn btn-danger btn-xs">删除</a> ';
|
||||
html += '<a href="/domain?cid='+row.id+'" class="btn btn-default btn-xs">域名</a>';
|
||||
return html;
|
||||
}
|
||||
},
|
||||
],
|
||||
onPageChange: function(number, size){
|
||||
if(size != defaultPageSize){
|
||||
setCookie('category_pagesize', size, 24 * 3600 * 30);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
$("#form-store").bootstrapValidator();
|
||||
});
|
||||
|
||||
function addframe(){
|
||||
$("#modal-store").modal('show');
|
||||
$("#modal-title").html("添加分类");
|
||||
$("#form-store input[name=id]").val('');
|
||||
$("#form-store input[name=name]").val('');
|
||||
$("#form-store input[name=sort]").val('0');
|
||||
$("#form-store input[name=remark]").val('');
|
||||
$("#form-store").data("bootstrapValidator").resetForm();
|
||||
}
|
||||
|
||||
function editframe(id){
|
||||
var row = $("#listTable").bootstrapTable('getRowByUniqueId', id);
|
||||
$("#modal-store").modal('show');
|
||||
$("#modal-title").html("修改分类");
|
||||
$("#form-store input[name=id]").val(id);
|
||||
$("#form-store input[name=name]").val(row.name);
|
||||
$("#form-store input[name=sort]").val(row.sort);
|
||||
$("#form-store input[name=remark]").val(row.remark);
|
||||
$("#form-store").data("bootstrapValidator").resetForm();
|
||||
}
|
||||
|
||||
function save(){
|
||||
$("#form-store").data("bootstrapValidator").validate();
|
||||
if(!$("#form-store").data("bootstrapValidator").isValid()){
|
||||
return;
|
||||
}
|
||||
var id = $("#form-store input[name=id]").val();
|
||||
var action = id ? 'edit' : 'add';
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/domain/category/' + action,
|
||||
data : $("#form-store").serialize(),
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.alert(data.msg, {
|
||||
icon: 1,
|
||||
closeBtn: false
|
||||
}, function(){
|
||||
layer.closeAll();
|
||||
$("#modal-store").modal('hide');
|
||||
searchRefresh();
|
||||
});
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function delItem(id) {
|
||||
layer.confirm('确定要删除此分类吗?', {title: '提示', icon: 0}, function(){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/domain/category/del',
|
||||
data : {id: id},
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.closeAll();
|
||||
layer.msg('删除成功', {icon: 1, time:800});
|
||||
searchRefresh();
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
@@ -107,12 +107,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right">备注</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="remark" placeholder="">
|
||||
</div>
|
||||
<label class="col-sm-3 control-label">所属分类</label>
|
||||
<div class="col-sm-9">
|
||||
<select name="cid" class="form-control">
|
||||
<option value="0">未分类</option>
|
||||
{foreach $categorys as $item}
|
||||
<option value="{$item.id}">{$item.name}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label no-padding-right">备注</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" name="remark" placeholder="">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">关闭</button>
|
||||
@@ -138,6 +149,9 @@
|
||||
<option value="{$k}">{$v}</option>
|
||||
{/foreach}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select name="cid" class="form-control"><option value="">所有分类</option>{foreach $categorys as $item}<option value="{$item.id}">{$item.name}</option>{/foreach}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select name="status" class="form-control"><option value="">所有状态</option><option value="1">即将到期</option><option value="2">已到期</option></select>
|
||||
</div>
|
||||
@@ -149,7 +163,7 @@
|
||||
{if $user['level'] eq 2}<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加</a>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">批量操作 <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu"><li><a href="/domain/add">添加域名</a></li><li><a href="javascript:operation('editremark')">修改域名备注</a></li><li><a href="javascript:operation('opennotice')">开启到期提醒</a></li><li><a href="javascript:operation('closenotice')">关闭到期提醒</a></li><li><a href="javascript:operation('updateexpire')">刷新到期时间</a></li><li><a href="javascript:operation('delete')">删除域名</a></li><li role="separator" class="divider"></li><li><a href="javascript:operation('addrecord')">添加解析</a></li><li><a href="javascript:operation('editrecord')">修改解析</a></li></ul>
|
||||
<ul class="dropdown-menu"><li><a href="/domain/add">添加域名</a></li><li><a href="javascript:operation('setcategory')">设置分类</a></li><li><a href="javascript:operation('editremark')">修改域名备注</a></li><li><a href="javascript:operation('opennotice')">开启到期提醒</a></li><li><a href="javascript:operation('closenotice')">关闭到期提醒</a></li><li><a href="javascript:operation('updateexpire')">刷新到期时间</a></li><li><a href="javascript:operation('delete')">删除域名</a></li><li role="separator" class="divider"></li><li><a href="javascript:operation('addrecord')">添加解析</a></li><li><a href="javascript:operation('editrecord')">修改解析</a></li><li><a href="/record/smartparse">智能添加解析</a></li></ul>
|
||||
</div>
|
||||
<a href="/domain/expirenotice" class="btn btn-default">到期提醒设置</a>{/if}
|
||||
</form>
|
||||
@@ -284,6 +298,13 @@ $(document).ready(function(){
|
||||
return value==1?'<font color="green">是</font>':'<font color="red">否</font>';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'category_name',
|
||||
title: '分类',
|
||||
formatter: function(value, row, index) {
|
||||
return value ? '<span class="label label-default">' + value + '</span>' : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注'
|
||||
@@ -400,6 +421,7 @@ function editframe(id){
|
||||
$("#form-store2 select[name=is_hide]").val(row.is_hide);
|
||||
$("#form-store2 select[name=is_sso]").val(row.is_sso);
|
||||
$("#form-store2 select[name=is_notice]").val(row.is_notice);
|
||||
$("#form-store2 select[name=cid]").val(row.cid ? row.cid : 0);
|
||||
$("#form-store2 input[name=remark]").val(row.remark);
|
||||
|
||||
$("#form-store2 input[name=expiretime]").datetimepicker({
|
||||
@@ -504,6 +526,9 @@ function operation(action){
|
||||
if(action == 'editremark'){
|
||||
batch_edit_remark(ids)
|
||||
return;
|
||||
}else if(action == 'setcategory'){
|
||||
batch_set_category(ids)
|
||||
return;
|
||||
}else if(action == 'addrecord'){
|
||||
sessionStorage.setItem('domains', JSON.stringify(rows));
|
||||
window.location.href = '/record/batchadd';
|
||||
@@ -607,6 +632,56 @@ function batch_edit_remark(ids) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function batch_set_category(ids) {
|
||||
var categoryOptions = '<option value="0">未分类</option>';
|
||||
$.ajax({
|
||||
type : 'GET',
|
||||
url : '/domain/category/list',
|
||||
dataType : 'json',
|
||||
async: false,
|
||||
success : function(data) {
|
||||
if(data.code == 0 && data.data){
|
||||
$.each(data.data, function(index, item){
|
||||
categoryOptions += '<option value="' + item.id + '">' + item.name + '</option>';
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
area: ['350px'],
|
||||
closeBtn: 2,
|
||||
title: '批量设置分类',
|
||||
content: '<div style="padding:15px"><div class="form-group"><select class="form-control" name="category_id">' + categoryOptions + '</select></div></div>',
|
||||
btn: ['确认', '取消'],
|
||||
yes: function(){
|
||||
var cid = $("select[name='category_id']").val();
|
||||
var ii = layer.load(2, {shade:[0.1,'#fff']});
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/domain/setcategory',
|
||||
data : {ids:ids, cid:cid},
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
layer.alert(data.msg,{
|
||||
icon: 1,
|
||||
closeBtn: false
|
||||
}, function(){
|
||||
layer.closeAll();
|
||||
searchRefresh();
|
||||
});
|
||||
},
|
||||
error:function(data){
|
||||
layer.close(ii);
|
||||
layer.msg('服务器错误');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function updateDate(id){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
<label class="col-sm-3 control-label">群机器人Webhook</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="expire_notice_webhook" default="{:config_get('expire_notice_webhook')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">自定义Webhook</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="expire_notice_custom_webhook" default="{:config_get('expire_notice_custom_webhook')}"><option value="0">关闭</option><option value="1">开启</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-9">
|
||||
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
|
||||
|
||||
@@ -183,7 +183,7 @@ td{overflow: hidden;text-overflow: ellipsis;white-space: nowrap;max-width:360px;
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> 搜索</button>
|
||||
<a href="javascript:searchClear()" class="btn btn-default" title="刷新解析记录列表"><i class="fa fa-refresh"></i> 刷新</a>
|
||||
<a href="javascript:addframe()" class="btn btn-success"><i class="fa fa-plus"></i> 添加记录</a>
|
||||
{if $dnsconfig.type=='cloudflare' && $user['level'] eq 2}<a href="/cloudflare/hostnames/{$domainId}" class="btn btn-default">Cloudflare增强</a>{/if}
|
||||
{if $dnsconfig.type=='cloudflare' && $user['level'] eq 2}<a href="/cloudflare/hostnames/{$domainId}" class="btn btn-default">Cloudflare自定义主机名</a>{/if}
|
||||
{if $dnsconfig.type=='aliyun'}<a href="/record/weight/{$domainId}" class="btn btn-default">权重配置</a>{/if}
|
||||
{if $dnsconfig.type=='dnspod'}<a href="/record/alias/{$domainId}" class="btn btn-default">域名别名</a>{/if}
|
||||
<div class="btn-group" role="group">
|
||||
@@ -350,6 +350,10 @@ $(document).ready(function(){
|
||||
if(dnsconfig.remark == 1){
|
||||
html += '<a href="javascript:setRemark(\''+row.RecordId+'\')" class="btn btn-info btn-xs">备注</a> ';
|
||||
}
|
||||
var supportedTypes = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'SRV', 'CAA', 'PTR', 'LOC', 'LUA', 'REDIRECT_URL', 'FORWARD_URL'];
|
||||
if(supportedTypes.includes(row.Type)){
|
||||
html += '<a href="javascript:checkRecord(\''+row.RecordId+'\')" class="btn btn-success btn-xs" title="检测解析生效"><i class="fa fa-check-circle-o"></i></a> ';
|
||||
}
|
||||
if(row.Type == 'A' || row.Type == 'CNAME' || row.Type == 'AAAA' || row.Type == 'REDIRECT_URL' || row.Type == 'FORWARD_URL'){
|
||||
if(row.Name === "@") var domain = "{$domainName}";
|
||||
else var domain = row.Name + ".{$domainName}";
|
||||
@@ -725,6 +729,48 @@ function advanceSearch(){
|
||||
$("#searchbox1").slideDown();
|
||||
}
|
||||
}
|
||||
function checkRecord(recordid) {
|
||||
var row = $("#listTable").bootstrapTable('getRowByUniqueId', recordid);
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/check/{$domainId}',
|
||||
data : {recordid: recordid, name: row.Name, type: row.Type, value: Array.isArray(row.Value) ? row.Value[0] : row.Value},
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
var result = data.data;
|
||||
var title = result.status === 'active' ? '<font color="green"><i class="fa fa-check-circle"></i> 解析已生效</font>' : (result.status === 'not_found' ? '<font color="red"><i class="fa fa-times-circle"></i> 未查询到解析</font>' : '<font color="red"><i class="fa fa-times-circle"></i> 解析值不匹配</font>');
|
||||
var content = '<div style="padding:0 10px;">';
|
||||
content += '<p><strong>主机记录:</strong>' + row.Name + '</p>';
|
||||
content += '<p><strong>记录类型:</strong>' + row.Type + '</p>';
|
||||
content += '<p><strong>记录值:</strong>' + htmlEscape(row.Value) + '</p>';
|
||||
content += '<hr style="margin:10px 0;">';
|
||||
content += '<p><strong>检测结果:</strong>' + title + '</p>';
|
||||
if(result.actual && result.actual.length > 0){
|
||||
content += '<p><strong>实际解析值:</strong></p>';
|
||||
content += '<ul style="max-height:150px;overflow-y:auto;">';
|
||||
for(var i = 0; i < result.actual.length; i++){
|
||||
content += '<li>' + htmlEscape(result.actual[i]) + '</li>';
|
||||
}
|
||||
content += '</ul>';
|
||||
}
|
||||
if(result.expected){
|
||||
content += '<p><strong>期望解析值:</strong>' + htmlEscape(result.expected) + '</p>';
|
||||
}
|
||||
content += '</div>';
|
||||
layer.alert(content, {title: 'DNS解析检测', area: ['450px'], shadeClose: true});
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.close(ii);
|
||||
layer.msg('服务器错误', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
function copyToClipboard(text, selector) {
|
||||
if (!text && selector) {
|
||||
var el = document.querySelector(selector);
|
||||
|
||||
825
app/view/domain/smartparse.html
Normal file
825
app/view/domain/smartparse.html
Normal file
@@ -0,0 +1,825 @@
|
||||
{extend name="common/layout" /}
|
||||
{block name="title"}智能批量添加{/block}
|
||||
{block name="main"}
|
||||
<style>
|
||||
.modal-body .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.batch-input-area {
|
||||
min-height: 200px;
|
||||
resize: vertical;
|
||||
}
|
||||
.batch-preview {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
}
|
||||
.batch-preview table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.batch-preview th,
|
||||
.batch-preview td {
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
border: 0.5px solid #f0f0f0;
|
||||
vertical-align: middle;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.batch-preview th {
|
||||
background-color: #f9f9f9;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.batch-preview tr:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.batch-preview .label {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.batch-preview .label-primary {
|
||||
background-color: #337ab7;
|
||||
color: #fff;
|
||||
}
|
||||
.batch-preview .status-success {
|
||||
color: #52c41a;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
.domain-select-modal {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.domain-item {
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 4px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.domain-item:hover {
|
||||
border-color: #337ab7;
|
||||
background-color: #f5f9fc;
|
||||
}
|
||||
.domain-item.selected {
|
||||
border-color: #337ab7;
|
||||
background-color: #e7f3ff;
|
||||
}
|
||||
.domain-item.selected::after {
|
||||
content: '✓';
|
||||
float: right;
|
||||
color: #337ab7;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 center-block" style="float: none;">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><a href="/domain" class="btn btn-sm btn-default pull-right" style="margin-top:-6px"><i class="fa fa-reply fa-fw"></i> 返回</a>智能批量添加解析</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" id="batchForm">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">批量数据 <span class="text-danger">*</span></label>
|
||||
<div class="col-sm-6">
|
||||
<textarea class="form-control batch-input-area" id="batchInput" rows="10"
|
||||
placeholder="请按以下格式输入(每行一条记录): 格式1:主机记录 记录值 格式2:主机记录 记录值 域名 格式3:记录值 主机记录.域名 格式4:主机记录.域名(使用下方记录值) 示例: www 1.2.3.4 example.com api app.example.com example.com 1.1.1.1 www.example.com example.com 说明: - 如果使用格式4,将使用下方的记录值 - 如果不指定域名,将使用下方选择的默认域名 - 如果检测到多个不同域名,会提示您选择对应的DNS配置"></textarea>
|
||||
<p class="help-block">每行一条记录,支持混合输入多个域名的记录</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">记录值</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" id="batchValueInput" placeholder="当使用格式4时,将使用此记录值">
|
||||
<p class="help-block">留空则不使用格式4</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">默认域名</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="defaultDomain" id="defaultDomainSelect" class="form-control select2">
|
||||
<option value="">不使用默认域名(必须每行都指定域名)</option>
|
||||
{foreach $domainList as $domain}
|
||||
<option value="{$domain.id}">{$domain.name} [{$domain.dnsType}]</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
<p class="help-block">当某行没有指定域名时,使用此默认域名</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">记录类型</label>
|
||||
<div class="col-sm-6">
|
||||
<select name="defaultType" id="defaultTypeSelect" class="form-control">
|
||||
<option value="">自动检测</option>
|
||||
<option value="A">A</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="NS">NS</option>
|
||||
<option value="MX">MX</option>
|
||||
<option value="SRV">SRV</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="CAA">CAA</option>
|
||||
</select>
|
||||
<p class="help-block">留空则根据记录值自动判断类型</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">线路</label>
|
||||
<div class="col-sm-6" id="batch_line_list">
|
||||
<select name="defaultLine" id="defaultLineSelect" class="form-control" onchange="changeBatchLine(this)">
|
||||
<option value="">自动选择</option>
|
||||
</select>
|
||||
<p class="help-block">留空则使用默认线路</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">TTL</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="number" class="form-control" name="defaultTtl" id="defaultTtlInput" value="600" min="1">
|
||||
<p class="help-block">默认TTL时间(秒)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-6">
|
||||
<button type="button" class="btn btn-info" onclick="previewBatchData()"><i class="fa fa-eye"></i> 预览解析结果</button>
|
||||
<button type="button" class="btn btn-primary" id="btnBatchAdd" onclick="submitBatchData()"><i class="fa fa-plus-circle"></i> 批量添加解析</button>
|
||||
<button type="button" class="btn btn-default" onclick="resetBatchForm()"><i class="fa fa-refresh"></i> 重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="form-group col-sm-12" id="previewSection" style="display:none;margin-top:20px;">
|
||||
<label>解析预览</label>
|
||||
<div class="table-responsive batch-preview">
|
||||
<table style="min-width: 800px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:5%">序号</th>
|
||||
<th style="width:15%">主机记录</th>
|
||||
<th style="width:10%">类型</th>
|
||||
<th style="width:25%">记录值</th>
|
||||
<th style="width:18%">DNS域名</th>
|
||||
<th style="width:12%">线路</th>
|
||||
<th style="width:8%">TTL</th>
|
||||
<th style="width:7%">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="previewBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="alert alert-info" id="previewSummary" style="margin-top:10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="modal-domain-select" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-md">
|
||||
<div class="modal-content animated flipInX">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
|
||||
<h4 class="modal-title">选择DNS配置</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fa fa-exclamation-triangle"></i> 检测到多个不同的域名,请为每个域名选择对应的DNS配置:
|
||||
</div>
|
||||
<div class="domain-select-modal" id="domainSelectModal">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-white" data-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="confirmDomainSelection()">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
{block name="script"}
|
||||
<script src="/static/js/layer/layer.js"></script>
|
||||
<script src="/static/js/select2-4.0.13.min.js"></script>
|
||||
<script>
|
||||
var domainList = [];
|
||||
{foreach $domainList as $domain}
|
||||
domainList.push({
|
||||
id: '{$domain.id}',
|
||||
name: '{$domain.name}',
|
||||
dnsType: '{$domain.dnsType}'
|
||||
});
|
||||
{/foreach}
|
||||
|
||||
var parsedBatchData = [];
|
||||
var domainMapping = {};
|
||||
|
||||
$(document).ready(function(){
|
||||
$('#defaultDomainSelect').select2({
|
||||
placeholder: '选择默认域名',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
language: {
|
||||
noResults: function(){ return '未找到匹配的域名'; },
|
||||
searching: function(){ return '搜索中...'; }
|
||||
}
|
||||
});
|
||||
|
||||
$('#defaultDomainSelect').on('change', function(){
|
||||
var domainId = $(this).val();
|
||||
if(domainId){
|
||||
var ii = layer.load(2);
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/quickinfo/' + domainId,
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
var lineOptions = '<option value="">自动选择</option>';
|
||||
var firstOption = null;
|
||||
$.each(data.data.recordLine, function(index, item){
|
||||
if(item.parent == null){
|
||||
if(!firstOption) firstOption = item.id;
|
||||
lineOptions += '<option value="'+item.id+'">'+item.name+'</option>';
|
||||
}
|
||||
});
|
||||
$('#batch_line_list').html('<select name="defaultLine" id="defaultLineSelect" class="form-control" onchange="changeBatchLine(this)">'+lineOptions+'</select>');
|
||||
window.currentRecordLine = data.data.recordLine;
|
||||
if(firstOption){
|
||||
$('#defaultLineSelect').val(firstOption).trigger('change');
|
||||
}
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2});
|
||||
}
|
||||
},
|
||||
error : function() {
|
||||
layer.close(ii);
|
||||
layer.alert('获取域名信息失败', {icon: 2});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function changeBatchLine(obj){
|
||||
var line = $(obj).val();
|
||||
var flag = false;
|
||||
$("#batch_line_list").children().each(function(index, elem){
|
||||
if(flag) $(elem).remove()
|
||||
if(obj == elem){ flag = true; }
|
||||
})
|
||||
if($(obj).find("option:selected").text() == '子集线路(非必填)') return;
|
||||
if(window.currentRecordLine){
|
||||
var tempLine = window.currentRecordLine.filter((x) => x.parent == line)
|
||||
if(tempLine.length > 0){
|
||||
var option = line.substr(0,2) == 'N.' ? '' : '<option value="'+line+'">子集线路(非必填)</option>';
|
||||
$.each(tempLine, function(index, item){
|
||||
option += '<option value="'+item.id+'">'+item.name+'</option>';
|
||||
})
|
||||
$("#batch_line_list").append('<select name="defaultLine" class="form-control" onchange="changeBatchLine(this)">'+option+'</select>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetBatchForm(){
|
||||
$('#batchInput').val('');
|
||||
$('#batchValueInput').val('');
|
||||
$('#defaultDomainSelect').val(null).trigger('change');
|
||||
$('#defaultTypeSelect').val('');
|
||||
$('#defaultTtlInput').val(600);
|
||||
$('#defaultLineSelect').val('').trigger('change');
|
||||
|
||||
$('#batch_line_list').empty();
|
||||
$('#batch_line_list').append('<select name="defaultLine" id="defaultLineSelect" class="form-control" onchange="changeBatchLine(this)"><option value="">自动选择</option></select>');
|
||||
|
||||
$('#previewSection').hide();
|
||||
parsedBatchData = [];
|
||||
domainMapping = {};
|
||||
}
|
||||
|
||||
function previewBatchData(){
|
||||
var inputText = $('#batchInput').val().trim();
|
||||
|
||||
if(!inputText){
|
||||
layer.alert('请输入批量解析数据', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = inputText.split('\n');
|
||||
var defaultDomainId = $('#defaultDomainSelect').val();
|
||||
var defaultType = $('#defaultTypeSelect').val();
|
||||
var defaultLine = $('#batch_line_list select[name=defaultLine]').last().val() || $('#defaultLineSelect').val();
|
||||
var defaultTtl = $('#defaultTtlInput').val();
|
||||
var batchValue = $('#batchValueInput').val();
|
||||
|
||||
parsedBatchData = [];
|
||||
domainMapping = {};
|
||||
var uniqueDomains = new Set();
|
||||
var errors = [];
|
||||
|
||||
$.each(lines, function(index, line){
|
||||
line = $.trim(line);
|
||||
if(!line) return;
|
||||
|
||||
var parts = line.split(/\s+/);
|
||||
|
||||
if(parts.length == 1 && batchValue){
|
||||
var domainPart = parts[0];
|
||||
var found = false;
|
||||
var host = '@';
|
||||
var domainName = domainPart;
|
||||
|
||||
var sortedDomains = domainList.slice().sort(function(a, b){
|
||||
return b.name.length - a.name.length;
|
||||
});
|
||||
|
||||
$.each(sortedDomains, function(i, domain){
|
||||
var dnsDomainName = domain.name;
|
||||
|
||||
if(domainPart === dnsDomainName){
|
||||
host = '@';
|
||||
domainName = domain.name;
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
else if(domainPart.endsWith('.' + dnsDomainName)){
|
||||
host = domainPart.substring(0, domainPart.length - (dnsDomainName.length + 1));
|
||||
domainName = domain.name;
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if(!found){
|
||||
errors.push('第' + (index + 1) + '行:域名 "' + domainPart + '" 不在你的域名列表中');
|
||||
return;
|
||||
}
|
||||
|
||||
value = batchValue;
|
||||
} else if(parts.length < 2){
|
||||
errors.push('第' + (index + 1) + '行格式错误:至少需要主机记录和记录值');
|
||||
return;
|
||||
} else if(parts.length == 2){
|
||||
var hostDomainPart = parts[1];
|
||||
var found = false;
|
||||
|
||||
var sortedDomains = domainList.slice().sort(function(a, b){
|
||||
return b.name.length - a.name.length;
|
||||
});
|
||||
|
||||
$.each(sortedDomains, function(i, domain){
|
||||
var dnsDomainName = domain.name;
|
||||
|
||||
if(hostDomainPart.endsWith('.' + dnsDomainName)){
|
||||
host = hostDomainPart.substring(0, hostDomainPart.length - (dnsDomainName.length + 1));
|
||||
value = parts[0];
|
||||
domainName = domain.name;
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if(!found){
|
||||
$.each(domainList, function(i, domain){
|
||||
if(hostDomainPart === domain.name){
|
||||
host = '@';
|
||||
value = parts[0];
|
||||
domainName = domain.name;
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if(!found){
|
||||
host = parts[0];
|
||||
value = parts[1];
|
||||
domainName = parts[2] || null;
|
||||
}
|
||||
}
|
||||
} else if(parts.length >= 2){
|
||||
host = parts[0];
|
||||
value = parts[1];
|
||||
domainName = parts[2] || null;
|
||||
}
|
||||
|
||||
var finalDomainId;
|
||||
var finalDomainName;
|
||||
|
||||
if(domainName){
|
||||
finalDomainName = domainName;
|
||||
|
||||
var foundDomain = null;
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.name.toLowerCase() === domainName.toLowerCase()){
|
||||
foundDomain = d;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if(foundDomain){
|
||||
finalDomainId = foundDomain.id;
|
||||
domainMapping[domainName] = foundDomain.id;
|
||||
}else{
|
||||
errors.push('第' + (index + 1) + '行:域名 "' + domainName + '" 不在你的域名列表中');
|
||||
return;
|
||||
}
|
||||
}else if(defaultDomainId){
|
||||
finalDomainId = defaultDomainId;
|
||||
var defaultDomainObj = null;
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.id === defaultDomainId){
|
||||
defaultDomainObj = d;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
finalDomainName = defaultDomainObj ? defaultDomainObj.name : '';
|
||||
}else{
|
||||
errors.push('第' + (index + 1) + '行:未指定域名且没有设置默认域名');
|
||||
return;
|
||||
}
|
||||
|
||||
uniqueDomains.add(finalDomainName);
|
||||
|
||||
var type = defaultType;
|
||||
if(!type){
|
||||
type = getDnsType(value);
|
||||
}
|
||||
|
||||
parsedBatchData.push({
|
||||
host: host,
|
||||
value: value,
|
||||
type: type,
|
||||
domainId: finalDomainId,
|
||||
domainName: finalDomainName,
|
||||
line: defaultLine,
|
||||
ttl: defaultTtl,
|
||||
lineNumber: index + 1,
|
||||
status: 'pending'
|
||||
});
|
||||
});
|
||||
|
||||
if(errors.length > 0){
|
||||
layer.alert('发现以下错误:\n\n' + errors.join('\n'), {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
if(parsedBatchData.length === 0){
|
||||
layer.alert('没有有效的解析记录', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
var uniqueDnsTypes = new Set();
|
||||
var domainDnsMap = {};
|
||||
|
||||
$.each(parsedBatchData, function(index, row){
|
||||
var domainInfo = null;
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.id === row.domainId){
|
||||
domainInfo = d;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if(domainInfo){
|
||||
uniqueDnsTypes.add(domainInfo.dnsType);
|
||||
domainDnsMap[row.domainId] = domainInfo.dnsType;
|
||||
}
|
||||
});
|
||||
|
||||
var uniqueDomainIds = new Set(parsedBatchData.map(r => r.domainId));
|
||||
|
||||
if(uniqueDomainIds.size > 1){
|
||||
showDomainSelectionModal(Array.from(uniqueDomainIds));
|
||||
return;
|
||||
}
|
||||
|
||||
renderPreview();
|
||||
}
|
||||
|
||||
function getDnsType(value){
|
||||
value = value.toLowerCase();
|
||||
if(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value)){
|
||||
return 'A';
|
||||
}else if(/^([a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i.test(value)){
|
||||
return 'CNAME';
|
||||
}else if(/^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/.test(value)){
|
||||
return 'AAAA';
|
||||
}else if(/^\d+$/.test(value) && parseInt(value) <= 65535){
|
||||
return 'MX';
|
||||
}else{
|
||||
return 'A';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreview(){
|
||||
var html = '';
|
||||
var validCount = 0;
|
||||
|
||||
$.each(parsedBatchData, function(index, row){
|
||||
var statusHtml = '<span class="status-success"><i class="fa fa-check"></i> 待添加</span>';
|
||||
validCount++;
|
||||
|
||||
html += '<tr>';
|
||||
html += '<td style="text-align:center;">' + row.lineNumber + '</td>';
|
||||
html += '<td>' + (row.host == '@' ? '@ (主域名)' : row.host) + '</td>';
|
||||
html += '<td style="text-align:center;"><span class="label label-primary">' + row.type + '</span></td>';
|
||||
html += '<td title="' + htmlEscape(row.value) + '">' + row.value + '</td>';
|
||||
html += '<td><strong>' + row.domainName + '</strong></td>';
|
||||
html += '<td style="text-align:center;">' + (row.line ? row.line : '默认') + '</td>';
|
||||
html += '<td style="text-align:center;">' + row.ttl + '</td>';
|
||||
html += '<td style="text-align:center;">' + statusHtml + '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
$('#previewBody').html(html);
|
||||
$('#previewSummary').html('<strong>共 ' + validCount + ' 条记录待添加</strong>');
|
||||
$('#previewSection').show();
|
||||
}
|
||||
|
||||
function showDomainSelectionModal(domains){
|
||||
var html = '';
|
||||
|
||||
$.each(domains, function(index, domainIdentifier){
|
||||
var domainName = '';
|
||||
var domainId = '';
|
||||
|
||||
if(!isNaN(domainIdentifier)){
|
||||
var domainInfo = null;
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.id === domainIdentifier){
|
||||
domainInfo = d;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if(domainInfo){
|
||||
domainName = domainInfo.name;
|
||||
domainId = domainInfo.id;
|
||||
}
|
||||
}else{
|
||||
domainName = domainIdentifier;
|
||||
var domainInfo = null;
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.name === domainIdentifier){
|
||||
domainInfo = d;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if(domainInfo){
|
||||
domainId = domainInfo.id;
|
||||
}
|
||||
}
|
||||
|
||||
if(!domainName) return;
|
||||
|
||||
var matches = [];
|
||||
$.each(domainList, function(i, d){
|
||||
if(d.name === domainName){
|
||||
matches.push(d);
|
||||
}
|
||||
});
|
||||
|
||||
if(matches.length === 0){
|
||||
matches = domainList;
|
||||
}
|
||||
|
||||
html += '<div style="margin-bottom:20px;">';
|
||||
html += '<h5><strong>' + domainName + '</strong></h5>';
|
||||
html += '<div class="row">';
|
||||
$.each(matches, function(j, match){
|
||||
var isSelected = j === 0;
|
||||
html += '<div class="col-md-6">';
|
||||
html += '<div class="domain-item' + (isSelected ? ' selected' : '') + '" ';
|
||||
html += 'data-domain="' + domainName + '" data-id="' + match.id + '" ';
|
||||
html += 'onclick="selectDomainItem(this)">';
|
||||
html += '<strong>' + match.name + '</strong> [' + match.dnsType + ']';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
if(isSelected){
|
||||
domainMapping[domainName] = match.id;
|
||||
}
|
||||
});
|
||||
html += '</div></div>';
|
||||
});
|
||||
|
||||
$('#domainSelectModal').html(html);
|
||||
$('#modal-domain-select').modal('show');
|
||||
}
|
||||
|
||||
function selectDomainItem(element){
|
||||
var $element = $(element);
|
||||
var domainName = $element.data('domain');
|
||||
var domainId = $element.data('id');
|
||||
|
||||
var $row = $element.closest('.row');
|
||||
$row.find('.domain-item').removeClass('selected');
|
||||
$element.addClass('selected');
|
||||
|
||||
domainMapping[domainName] = domainId;
|
||||
}
|
||||
|
||||
function confirmDomainSelection(){
|
||||
$('#modal-domain-select').modal('hide');
|
||||
|
||||
$.each(parsedBatchData, function(index, row){
|
||||
if(domainMapping[row.domainName]){
|
||||
row.domainId = domainMapping[row.domainName];
|
||||
}
|
||||
});
|
||||
|
||||
renderPreview();
|
||||
|
||||
layer.msg('域名配置已更新', {icon: 1, time: 1500});
|
||||
}
|
||||
|
||||
function submitBatchData(){
|
||||
if(parsedBatchData.length === 0){
|
||||
layer.alert('请先预览解析结果', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
layer.confirm('确定要批量添加这 <strong>' + parsedBatchData.length + '</strong> 条解析记录吗?', {
|
||||
title: '确认批量添加',
|
||||
icon: 0,
|
||||
btn: ['确定添加', '取消']
|
||||
}, function(){
|
||||
executeBatchAdd();
|
||||
});
|
||||
}
|
||||
|
||||
function executeBatchAdd(){
|
||||
var groupedByDomain = {};
|
||||
$.each(parsedBatchData, function(index, row){
|
||||
if(!groupedByDomain[row.domainId]){
|
||||
groupedByDomain[row.domainId] = [];
|
||||
}
|
||||
groupedByDomain[row.domainId].push(row);
|
||||
});
|
||||
|
||||
var totalSuccess = 0;
|
||||
var totalFail = 0;
|
||||
var completedCount = 0;
|
||||
var totalCount = Object.keys(groupedByDomain).length;
|
||||
var failReasons = [];
|
||||
|
||||
var $btn = $('#btnBatchAdd');
|
||||
var btnOrigHtml = $btn.html();
|
||||
$btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> 正在添加...');
|
||||
var ii = layer.load(2);
|
||||
|
||||
$.each(groupedByDomain, function(domainId, records){
|
||||
var recordLines = [];
|
||||
$.each(records, function(i, r){
|
||||
recordLines.push(r.host + ' ' + r.value);
|
||||
});
|
||||
var recordStr = recordLines.join('\n');
|
||||
|
||||
$.ajax({
|
||||
type : 'POST',
|
||||
url : '/record/batchadd/' + domainId,
|
||||
data : function(){
|
||||
var data = {
|
||||
record: recordStr,
|
||||
type: records[0].type,
|
||||
ttl: records[0].ttl
|
||||
};
|
||||
if(records[0].line){
|
||||
data.line = records[0].line;
|
||||
}
|
||||
return data;
|
||||
}(),
|
||||
dataType : 'json',
|
||||
async: false,
|
||||
success : function(data) {
|
||||
completedCount++;
|
||||
if(data.code == 0){
|
||||
var match = data.msg.match(/成功(\d+)条/);
|
||||
if(match){
|
||||
totalSuccess += parseInt(match[1]);
|
||||
}
|
||||
var failMatch = data.msg.match(/失败(\d+)条/);
|
||||
if(failMatch){
|
||||
var failCount = parseInt(failMatch[1]);
|
||||
totalFail += failCount;
|
||||
|
||||
if(failCount > 0){
|
||||
var startIndex = records.length - failCount;
|
||||
for(var i = startIndex; i < records.length; i++){
|
||||
var record = records[i];
|
||||
failReasons.push('记录 ' + record.host + ' [域名: ' + record.domainName + ']:' + data.msg);
|
||||
}
|
||||
}
|
||||
} else if(data.msg.indexOf('失败') !== -1){
|
||||
failReasons.push('域名 ' + records[0].domainName + ':' + data.msg);
|
||||
}
|
||||
}else{
|
||||
totalFail += records.length;
|
||||
$.each(records, function(i, record){
|
||||
failReasons.push('记录 ' + record.host + ' [域名: ' + record.domainName + ']:' + data.msg);
|
||||
});
|
||||
}
|
||||
|
||||
if(completedCount >= totalCount){
|
||||
layer.close(ii);
|
||||
$btn.prop('disabled', false).html(btnOrigHtml);
|
||||
|
||||
var msg = '批量添加完成!';
|
||||
if(totalSuccess > 0){
|
||||
msg += '\n成功:' + totalSuccess + ' 条';
|
||||
}
|
||||
if(totalFail > 0){
|
||||
msg += '\n失败:' + totalFail + ' 条';
|
||||
if(failReasons.length > 0){
|
||||
msg += '\n\n失败原因:';
|
||||
$.each(failReasons, function(i, reason){
|
||||
msg += '\n' + (i + 1) + '. ' + reason;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
layer.alert(msg, {
|
||||
icon: totalFail > 0 ? 2 : 1,
|
||||
btn: ['确定'],
|
||||
yes: function(index){
|
||||
layer.close(index);
|
||||
try {
|
||||
resetBatchForm();
|
||||
} catch(e) {
|
||||
console.error('Error in callback:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error : function() {
|
||||
completedCount++;
|
||||
totalFail += records.length;
|
||||
$.each(records, function(i, record){
|
||||
failReasons.push('记录 ' + record.host + ' [域名: ' + record.domainName + ']:网络错误,无法连接服务器');
|
||||
});
|
||||
|
||||
if(completedCount >= totalCount){
|
||||
layer.close(ii);
|
||||
$btn.prop('disabled', false).html(btnOrigHtml);
|
||||
var msg = '批量添加完成!';
|
||||
if(totalSuccess > 0){
|
||||
msg += '\n成功:' + totalSuccess + ' 条';
|
||||
}
|
||||
if(totalFail > 0){
|
||||
msg += '\n失败:' + totalFail + ' 条';
|
||||
if(failReasons.length > 0){
|
||||
msg += '\n\n失败原因:';
|
||||
$.each(failReasons, function(i, reason){
|
||||
msg += '\n' + (i + 1) + '. ' + reason;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
layer.alert(msg, {
|
||||
icon: totalFail > 0 ? 2 : 1,
|
||||
btn: ['确定'],
|
||||
yes: function(index){
|
||||
layer.close(index);
|
||||
try {
|
||||
resetBatchForm();
|
||||
} catch(e) {
|
||||
console.error('Error in callback:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function htmlEscape(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
@@ -139,6 +139,44 @@
|
||||
@用户不支持企业微信,飞书用户手机号需要填写<a href="https://open.feishu.cn/document/home/user-identity-introduction/open-id" target="_blank" rel="noreferrer">用户ID</a>。
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading"><h3 class="panel-title">自定义Webhook</h3></div>
|
||||
<div class="panel-body">
|
||||
<form onsubmit="return saveSetting(this)" method="post" class="form-horizontal" role="form">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Webhook地址</label>
|
||||
<div class="col-sm-9"><input type="text" name="custom_webhook_url" value="{:config_get('custom_webhook_url')}" class="form-control" placeholder="https://example.com/webhook"/></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">请求方式</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="custom_webhook_method" default="{:config_get('custom_webhook_method', 'POST')}"><option value="POST">POST</option><option value="GET">GET</option><option value="PUT">PUT</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Content-Type</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="custom_webhook_content_type" default="{:config_get('custom_webhook_content_type', 'application/json')}"><option value="application/json">application/json</option><option value="application/x-www-form-urlencoded">application/x-www-form-urlencoded</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">自定义Headers</label>
|
||||
<div class="col-sm-9"><textarea name="custom_webhook_headers" class="form-control" rows="3" placeholder='每行一个,格式:HeaderName: HeaderValue'>{:config_get('custom_webhook_headers')}</textarea></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">请求Body</label>
|
||||
<div class="col-sm-9"><textarea name="custom_webhook_body" class="form-control" rows="4">{php}echo htmlspecialchars(config_get('custom_webhook_body') ?: '{"title":"{title}","content":"{content}"}');{/php}</textarea>
|
||||
<font color="green">支持变量:{title}、{content},如果是GET方式,将作为query参数拼接到url上</font></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">消息内容格式</label>
|
||||
<div class="col-sm-9"><select class="form-control" name="custom_webhook_content_format" default="{:config_get('custom_webhook_content_format', 'html')}"><option value="html">HTML</option><option value="markdown">Markdown</option><option value="text">纯文本</option></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-3 col-sm-9">
|
||||
<input type="submit" name="submit" value="保存" class="btn btn-primary btn-block"/>
|
||||
<a href="javascript:customwebhooktest()" class="btn btn-default btn-block">发送测试消息</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
@@ -254,5 +292,25 @@ function webhooktest(){
|
||||
}
|
||||
});
|
||||
}
|
||||
function customwebhooktest(){
|
||||
var ii = layer.load(2, {shade:[0.1,'#fff']});
|
||||
$.ajax({
|
||||
type : 'GET',
|
||||
url : '/system/customwebhooktest',
|
||||
dataType : 'json',
|
||||
success : function(data) {
|
||||
layer.close(ii);
|
||||
if(data.code == 0){
|
||||
layer.alert(data.msg, {icon: 1});
|
||||
}else{
|
||||
layer.alert(data.msg, {icon: 2})
|
||||
}
|
||||
},
|
||||
error:function(data){
|
||||
layer.close(ii);
|
||||
layer.msg('服务器错误');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{/block}
|
||||
20
composer.lock
generated
20
composer.lock
generated
@@ -1036,7 +1036,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-idn",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-idn.git",
|
||||
@@ -1099,7 +1099,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1123,7 +1123,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
@@ -1184,7 +1184,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1208,7 +1208,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
@@ -1269,7 +1269,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1293,7 +1293,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php81",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php81.git",
|
||||
@@ -1349,7 +1349,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -1373,7 +1373,7 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php82",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.37.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php82.git",
|
||||
@@ -1429,7 +1429,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.34.0"
|
||||
"source": "https://github.com/symfony/polyfill-php82/tree/v1.37.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ return [
|
||||
'show_error_msg' => true,
|
||||
'exception_tmpl' => \think\facade\App::getAppPath() . 'view/exception.tpl',
|
||||
|
||||
'version' => '1049',
|
||||
'version' => '1050',
|
||||
|
||||
'dbversion' => '1048'
|
||||
'dbversion' => '1049'
|
||||
];
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
|
||||
RewriteRule ^(.*)$ index.php [L,E=PATH_INFO:/$1]
|
||||
</IfModule>
|
||||
|
||||
1
public/static/images/npm.svg
Normal file
1
public/static/images/npm.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
BIN
public/static/images/technitium.png
Normal file
BIN
public/static/images/technitium.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 325 B |
@@ -55,12 +55,17 @@ Route::group(function () {
|
||||
Route::post('/cloudflare/hostnames/data/:id', 'cloudflare/hostnames_data');
|
||||
Route::post('/cloudflare/hostnames/add/:id', 'cloudflare/hostnames_add');
|
||||
Route::post('/cloudflare/hostnames/update/:id', 'cloudflare/hostnames_update');
|
||||
Route::post('/cloudflare/hostnames/refresh/:id', 'cloudflare/hostnames_refresh');
|
||||
Route::post('/cloudflare/hostnames/delete/:id', 'cloudflare/hostnames_delete');
|
||||
Route::post('/cloudflare/hostnames/refresh/:id', 'cloudflare/hostnames_refresh');
|
||||
Route::post('/cloudflare/hostnames/txttargets/:id', 'cloudflare/hostnames_txt_targets');
|
||||
Route::post('/cloudflare/hostnames/batch_add/:id', 'cloudflare/hostnames_batch_add');
|
||||
Route::post('/cloudflare/hostnames/batch_delete/:id', 'cloudflare/hostnames_batch_delete');
|
||||
Route::post('/cloudflare/hostnames/batch_update/:id', 'cloudflare/hostnames_batch_update');
|
||||
Route::post('/cloudflare/fallback/get/:id', 'cloudflare/fallback_get');
|
||||
Route::post('/cloudflare/fallback/set/:id', 'cloudflare/fallback_set');
|
||||
Route::post('/cloudflare/fallback/delete/:id', 'cloudflare/fallback_delete');
|
||||
Route::post('/cloudflare/dcv_delegation_uuid/:id', 'cloudflare/dcv_delegation_uuid');
|
||||
Route::post('/cloudflare/get_domain_default_line', 'cloudflare/get_domain_default_line');
|
||||
Route::get('/cloudflare/tunnels/:id', 'cloudflare/tunnels');
|
||||
Route::post('/cloudflare/tunnels/data/:id', 'cloudflare/tunnels_data');
|
||||
Route::post('/cloudflare/tunnels/add/:id', 'cloudflare/tunnels_add');
|
||||
@@ -81,7 +86,13 @@ Route::group(function () {
|
||||
Route::post('/domain/data', 'domain/domain_data');
|
||||
Route::post('/domain/op', 'domain/domain_op');
|
||||
Route::post('/domain/list', 'domain/domain_list');
|
||||
Route::any('/domain/dnscheck', 'domain/dnscheck');
|
||||
Route::post('/domain/category/data', 'domain/category_data');
|
||||
Route::post('/domain/category/:action', 'domain/category_op');
|
||||
Route::get('/domain/category/list', 'domain/category_list');
|
||||
Route::post('/domain/setcategory', 'domain/domain_set_category');
|
||||
Route::get('/domain/add', 'domain/domain_add');
|
||||
Route::get('/domain/category', 'domain/category');
|
||||
Route::get('/domain', 'domain/domain');
|
||||
|
||||
Route::post('/record/data/:id', 'domain/record_data');
|
||||
@@ -90,6 +101,7 @@ Route::group(function () {
|
||||
Route::post('/record/delete/:id', 'domain/record_delete');
|
||||
Route::post('/record/status/:id', 'domain/record_status');
|
||||
Route::post('/record/remark/:id', 'domain/record_remark');
|
||||
Route::post('/record/check/:id', 'domain/record_check');
|
||||
Route::post('/record/batch/:id', 'domain/record_batch');
|
||||
Route::post('/record/batchedit/:id', 'domain/record_batch_edit');
|
||||
Route::any('/record/batchadd/:id', 'domain/record_batch_add');
|
||||
@@ -101,6 +113,8 @@ Route::group(function () {
|
||||
Route::any('/record/weight/:id', 'domain/weight');
|
||||
Route::any('/record/alias/:id', 'domain/alias');
|
||||
Route::get('/record/:id', 'domain/record');
|
||||
Route::get('/record/smartparse', 'domain/smartparse');
|
||||
Route::post('/record/quickinfo/:id', 'domain/quickinfo');
|
||||
|
||||
Route::get('/dmonitor/overview', 'dmonitor/overview');
|
||||
Route::post('/dmonitor/task/data', 'dmonitor/task_data');
|
||||
@@ -153,6 +167,7 @@ Route::group(function () {
|
||||
Route::get('/system/mailtest', 'system/mailtest');
|
||||
Route::get('/system/tgbottest', 'system/tgbottest');
|
||||
Route::get('/system/webhooktest', 'system/webhooktest');
|
||||
Route::get('/system/customwebhooktest', 'system/customwebhooktest');
|
||||
Route::post('/system/proxytest', 'system/proxytest');
|
||||
Route::get('/system/cronset', 'system/cronset');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user