Compare commits
63 commits
483c8d8965
...
4ced1bb61c
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ced1bb61c | |||
| f2d004a520 | |||
| 12647ae3b7 | |||
| e9ee0328ac | |||
| 70f7a9c9f8 | |||
| 9cccdd3e8f | |||
| cbafcab983 | |||
| f3108c23d2 | |||
| 2118801fb7 | |||
| 9433c41233 | |||
| e682553280 | |||
| cc2d1cab88 | |||
| f8a4757f05 | |||
| db4efe88f0 | |||
| cece77eb82 | |||
| f10729e055 | |||
| 92c2676160 | |||
| 33c3aca16d | |||
| 1260e274e7 | |||
| 93a4b85c5e | |||
| 6322087807 | |||
| 2b9bfab1b1 | |||
| 11538f73e2 | |||
| e79f080bb6 | |||
| 9aa0d13758 | |||
| 3e27104c09 | |||
| 561df0e4cf | |||
| b730a637e6 | |||
| 36920e2c5b | |||
| 225c63362c | |||
| 4a7e5a5a00 | |||
| 8dc2a7740d | |||
| 8cee08b7ef | |||
| 4e5c63df71 | |||
| 5bdecaad3a | |||
| 62811a42bd | |||
| 0bfee7f23b | |||
| dfb8dfb6f8 | |||
| dc2a0d521f | |||
| abcda2dad0 | |||
| f45315d7c7 | |||
| 57655acc13 | |||
| 618a86bd37 | |||
| 983634caf5 | |||
| ae5d77b5fc | |||
| 73bacd08a0 | |||
| b4ded8dabe | |||
| 50046d0e21 | |||
| 6f39c7bbf9 | |||
| 7c483e1a4d | |||
| a92920111c | |||
| b727d461ee | |||
| d25ddb42d2 | |||
| 50589c9a28 | |||
| 9edb3749c2 | |||
| be3c89cab4 | |||
| ca8882c3f5 | |||
| 63344444d7 | |||
| 40f3226132 | |||
| 5969b2f131 | |||
| 0c10e306ac | |||
| 19814805f7 | |||
| 6f85fd4dd5 |
106 changed files with 17571 additions and 21622 deletions
|
|
@ -1,119 +0,0 @@
|
|||
name: Auto Label
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
auto-label:
|
||||
if: github.repository != 'WenPai-org/ci-workflows'
|
||||
runs-on: linux-arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 自动打标签
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
API_BASE="${GITHUB_SERVER_URL:-https://feicode.com}/api/v1"
|
||||
OWNER="${GITHUB_REPOSITORY%/*}"
|
||||
REPO="${GITHUB_REPOSITORY#*/}"
|
||||
LABELS=""
|
||||
|
||||
# 获取 PR 变更文件
|
||||
FILES=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/pulls/$PR_NUMBER/files" | \
|
||||
python3 -c "import json,sys; [print(f['filename']) for f in json.load(sys.stdin)]" 2>/dev/null)
|
||||
|
||||
# 根据文件类型打标签
|
||||
echo "$FILES" | grep -qE '\.php$' && LABELS="$LABELS,php"
|
||||
echo "$FILES" | grep -qE '\.(js|ts|tsx|jsx)$' && LABELS="$LABELS,frontend"
|
||||
echo "$FILES" | grep -qE '\.(css|scss|less)$' && LABELS="$LABELS,style"
|
||||
echo "$FILES" | grep -qE '\.go$' && LABELS="$LABELS,go"
|
||||
echo "$FILES" | grep -qE '\.(yml|yaml)$' && LABELS="$LABELS,ci/cd"
|
||||
echo "$FILES" | grep -qE '(composer\.|package\.json|go\.mod)' && LABELS="$LABELS,dependencies"
|
||||
echo "$FILES" | grep -qE '\.(md|txt|rst)$' && LABELS="$LABELS,documentation"
|
||||
echo "$FILES" | grep -qE '(Dockerfile|docker-compose)' && LABELS="$LABELS,docker"
|
||||
|
||||
# 根据 PR 大小打标签
|
||||
ADDITIONS=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/pulls/$PR_NUMBER" | \
|
||||
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('additions',0)+d.get('deletions',0))" 2>/dev/null)
|
||||
|
||||
if [ "$ADDITIONS" -lt 10 ] 2>/dev/null; then
|
||||
LABELS="$LABELS,size/S"
|
||||
elif [ "$ADDITIONS" -lt 100 ] 2>/dev/null; then
|
||||
LABELS="$LABELS,size/M"
|
||||
elif [ "$ADDITIONS" -lt 500 ] 2>/dev/null; then
|
||||
LABELS="$LABELS,size/L"
|
||||
else
|
||||
LABELS="$LABELS,size/XL"
|
||||
fi
|
||||
|
||||
# 去掉开头逗号
|
||||
LABELS="${LABELS#,}"
|
||||
|
||||
if [ -z "$LABELS" ]; then
|
||||
echo "无需添加标签"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "添加标签: $LABELS"
|
||||
|
||||
# 确保标签存在,不存在则创建
|
||||
IFS=',' read -ra LABEL_ARRAY <<< "$LABELS"
|
||||
LABEL_IDS="["
|
||||
for LABEL in "${LABEL_ARRAY[@]}"; do
|
||||
# 查找标签
|
||||
LABEL_ID=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/labels?limit=50" | \
|
||||
python3 -c "
|
||||
import json,sys
|
||||
labels=json.load(sys.stdin)
|
||||
for l in labels:
|
||||
if l['name']=='$LABEL':
|
||||
print(l['id'])
|
||||
break
|
||||
" 2>/dev/null)
|
||||
|
||||
# 标签不存在则创建
|
||||
if [ -z "$LABEL_ID" ]; then
|
||||
# 根据标签类型选颜色
|
||||
case "$LABEL" in
|
||||
php) COLOR="#4F5D95" ;;
|
||||
frontend) COLOR="#f1e05a" ;;
|
||||
go) COLOR="#00ADD8" ;;
|
||||
ci/cd) COLOR="#0075ca" ;;
|
||||
dependencies) COLOR="#0366d6" ;;
|
||||
documentation) COLOR="#0075ca" ;;
|
||||
docker) COLOR="#0db7ed" ;;
|
||||
style) COLOR="#e34c26" ;;
|
||||
size/S) COLOR="#009900" ;;
|
||||
size/M) COLOR="#FFCC00" ;;
|
||||
size/L) COLOR="#FF6600" ;;
|
||||
size/XL) COLOR="#CC0000" ;;
|
||||
*) COLOR="#ededed" ;;
|
||||
esac
|
||||
LABEL_ID=$(curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/labels" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$LABEL\",\"color\":\"$COLOR\"}" | \
|
||||
python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
|
||||
fi
|
||||
|
||||
[ -n "$LABEL_ID" ] && LABEL_IDS="$LABEL_IDS$LABEL_ID,"
|
||||
done
|
||||
LABEL_IDS="${LABEL_IDS%,}]"
|
||||
|
||||
# 添加标签到 PR
|
||||
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/issues/$PR_NUMBER/labels" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"labels\":$LABEL_IDS}" > /dev/null
|
||||
|
||||
echo "标签添加完成"
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
name: gitleaks 密钥泄露扫描
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['*']
|
||||
pull_request:
|
||||
branches: ['*']
|
||||
|
||||
jobs:
|
||||
gitleaks:
|
||||
runs-on: linux-arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run gitleaks
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
|
||||
gitleaks detect --source=. --log-opts="$GITHUB_SHA~1..$GITHUB_SHA" --verbose --exit-code 1 || {
|
||||
echo "::error::gitleaks 发现了潜在的密钥泄露!请检查上方输出并移除敏感信息。"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
gitleaks detect --source=. --verbose --exit-code 1 || {
|
||||
echo "::error::gitleaks 发现了潜在的密钥泄露!请检查上方输出并移除敏感信息。"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
echo "gitleaks 扫描通过,未发现密钥泄露。"
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
name: 安全扫描
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main', 'master']
|
||||
paths:
|
||||
- 'Dockerfile*'
|
||||
- 'docker-compose*.yml'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'yarn.lock'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
pull_request:
|
||||
branches: ['main', 'master']
|
||||
schedule:
|
||||
- cron: '0 3 * * 1' # 每周一凌晨 3 点
|
||||
|
||||
jobs:
|
||||
security-scan:
|
||||
runs-on: linux-arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Hadolint Dockerfile 检查
|
||||
run: |
|
||||
# 检查是否存在 Dockerfile
|
||||
DOCKERFILES=$(find . -name 'Dockerfile*' -not -path './.git/*' 2>/dev/null)
|
||||
if [ -z "$DOCKERFILES" ]; then
|
||||
echo "未找到 Dockerfile,跳过"
|
||||
exit 0
|
||||
fi
|
||||
# 如果未安装 hadolint 则跳过
|
||||
if ! command -v hadolint &> /dev/null; then
|
||||
echo "hadolint 未安装,跳过 Dockerfile 检查"
|
||||
exit 0
|
||||
fi
|
||||
FAILED=0
|
||||
for df in $DOCKERFILES; do
|
||||
echo "--- 检查 $df ---"
|
||||
hadolint "$df" || FAILED=1
|
||||
done
|
||||
if [ $FAILED -eq 1 ]; then
|
||||
echo "::warning::Dockerfile 存在规范问题,请检查上方输出"
|
||||
fi
|
||||
|
||||
- name: PHP Composer 安全审计
|
||||
run: |
|
||||
if [ ! -f composer.lock ]; then
|
||||
echo "未找到 composer.lock,跳过"
|
||||
exit 0
|
||||
fi
|
||||
composer audit --format=table || {
|
||||
echo "::error::Composer 依赖存在已知安全漏洞"
|
||||
exit 1
|
||||
}
|
||||
echo "Composer 安全审计通过"
|
||||
|
||||
- name: npm 安全审计
|
||||
run: |
|
||||
if [ ! -f package-lock.json ] && [ ! -f yarn.lock ] && [ ! -f pnpm-lock.yaml ]; then
|
||||
echo "未找到 JS 锁文件,跳过"
|
||||
exit 0
|
||||
fi
|
||||
# npm audit 只报告 high 和 critical
|
||||
if [ -f package-lock.json ]; then
|
||||
npm audit --audit-level=high || {
|
||||
echo "::error::npm 依赖存在高危安全漏洞"
|
||||
exit 1
|
||||
}
|
||||
elif [ -f pnpm-lock.yaml ]; then
|
||||
pnpm audit --audit-level=high || {
|
||||
echo "::error::pnpm 依赖存在高危安全漏洞"
|
||||
exit 1
|
||||
}
|
||||
elif [ -f yarn.lock ]; then
|
||||
yarn npm audit --severity high || {
|
||||
echo "::error::yarn 依赖存在高危安全漏洞"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
echo "JS 依赖安全审计通过"
|
||||
|
||||
- name: Go 依赖漏洞检查
|
||||
run: |
|
||||
if [ ! -f go.mod ]; then
|
||||
echo "未找到 go.mod,跳过"
|
||||
exit 0
|
||||
fi
|
||||
# govulncheck 检查已知漏洞
|
||||
if ! command -v govulncheck &> /dev/null; then
|
||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
fi
|
||||
govulncheck ./... || {
|
||||
echo "::error::Go 依赖存在已知安全漏洞"
|
||||
exit 1
|
||||
}
|
||||
echo "Go 依赖安全检查通过"
|
||||
|
||||
- name: Trivy 容器镜像扫描
|
||||
run: |
|
||||
DOCKERFILES=$(find . -name 'Dockerfile' -not -path './.git/*' 2>/dev/null)
|
||||
if [ -z "$DOCKERFILES" ]; then
|
||||
echo "未找到 Dockerfile,跳过镜像扫描"
|
||||
exit 0
|
||||
fi
|
||||
if ! command -v trivy &> /dev/null; then
|
||||
echo "trivy 未安装,跳过容器镜像扫描"
|
||||
exit 0
|
||||
fi
|
||||
# 构建并扫描镜像
|
||||
IMAGE_NAME="ci-security-scan:$$"
|
||||
podman build -t "$IMAGE_NAME" -f Dockerfile . || {
|
||||
echo "::warning::容器构建失败,跳过镜像扫描"
|
||||
exit 0
|
||||
}
|
||||
trivy image --severity HIGH,CRITICAL --exit-code 1 "$IMAGE_NAME" || {
|
||||
echo "::error::容器镜像存在高危漏洞"
|
||||
podman rmi "$IMAGE_NAME" 2>/dev/null
|
||||
exit 1
|
||||
}
|
||||
podman rmi "$IMAGE_NAME" 2>/dev/null
|
||||
echo "容器镜像安全扫描通过"
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
name: Stale Issue/PR 清理
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * 1' # 每周一凌晨 5 点
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
stale-cleanup:
|
||||
if: github.repository != 'WenPai-org/ci-workflows'
|
||||
runs-on: linux-arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 清理过期 Issue 和 PR
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
API_BASE="${GITHUB_SERVER_URL:-https://feicode.com}/api/v1"
|
||||
OWNER="${GITHUB_REPOSITORY%/*}"
|
||||
REPO="${GITHUB_REPOSITORY#*/}"
|
||||
NOW=$(date +%s)
|
||||
|
||||
# 受保护的标签,带这些标签的不处理
|
||||
PROTECTED_LABELS="pinned|help-wanted|bug|security|wontfix"
|
||||
|
||||
# Issue: 60 天无活动标记 stale,再过 14 天关闭
|
||||
STALE_DAYS=60
|
||||
CLOSE_DAYS=74
|
||||
# PR: 30 天无活动标记 stale,再过 14 天关闭
|
||||
PR_STALE_DAYS=30
|
||||
PR_CLOSE_DAYS=44
|
||||
|
||||
process_items() {
|
||||
local TYPE=$1 # issues 或 pulls
|
||||
local STALE=$2
|
||||
local CLOSE=$3
|
||||
|
||||
PAGE=1
|
||||
while true; do
|
||||
if [ "$TYPE" = "issues" ]; then
|
||||
ITEMS=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/issues?state=open&type=issues&page=$PAGE&limit=50")
|
||||
else
|
||||
ITEMS=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/issues?state=open&type=pulls&page=$PAGE&limit=50")
|
||||
fi
|
||||
|
||||
COUNT=$(echo "$ITEMS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
||||
[ "$COUNT" = "0" ] || [ -z "$COUNT" ] && break
|
||||
|
||||
echo "$ITEMS" | python3 -c "
|
||||
import json, sys, time
|
||||
|
||||
items = json.load(sys.stdin)
|
||||
now = $NOW
|
||||
stale_seconds = $STALE * 86400
|
||||
close_seconds = $CLOSE * 86400
|
||||
protected = set('$PROTECTED_LABELS'.split('|'))
|
||||
|
||||
for item in items:
|
||||
number = item['number']
|
||||
title = item['title']
|
||||
labels = [l['name'] for l in item.get('labels', [])]
|
||||
|
||||
# 跳过受保护标签
|
||||
if protected & set(labels):
|
||||
continue
|
||||
|
||||
updated = item['updated_at']
|
||||
# 解析 ISO 时间
|
||||
from datetime import datetime, timezone
|
||||
dt = datetime.fromisoformat(updated.replace('Z', '+00:00'))
|
||||
updated_ts = int(dt.timestamp())
|
||||
age = now - updated_ts
|
||||
|
||||
if age > close_seconds:
|
||||
print(f'CLOSE|{number}|{title}')
|
||||
elif age > stale_seconds and 'stale' not in labels:
|
||||
print(f'STALE|{number}|{title}')
|
||||
" | while IFS='|' read -r ACTION NUMBER TITLE; do
|
||||
if [ "$ACTION" = "STALE" ]; then
|
||||
echo "标记 #$NUMBER 为 stale: $TITLE"
|
||||
# 添加 stale 标签
|
||||
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER/labels" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"labels":["stale"]}' > /dev/null
|
||||
# 添加评论
|
||||
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER/comments" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"body\":\"此 ${TYPE%s} 已超过 $STALE 天无活动,已标记为 stale。如果 14 天内无新活动将自动关闭。\"}" > /dev/null
|
||||
elif [ "$ACTION" = "CLOSE" ]; then
|
||||
echo "关闭 #$NUMBER: $TITLE"
|
||||
curl -s -X PATCH -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"state":"closed"}' > /dev/null
|
||||
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER/comments" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"body\":\"此 ${TYPE%s} 因长期无活动已自动关闭。如需继续讨论请重新打开。\"}" > /dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
PAGE=$((PAGE + 1))
|
||||
done
|
||||
}
|
||||
|
||||
echo "=== 处理 Issues (${STALE_DAYS}天stale / ${CLOSE_DAYS}天关闭) ==="
|
||||
process_items "issues" $STALE_DAYS $CLOSE_DAYS
|
||||
|
||||
echo "=== 处理 PRs (${PR_STALE_DAYS}天stale / ${PR_CLOSE_DAYS}天关闭) ==="
|
||||
process_items "pulls" $PR_STALE_DAYS $PR_CLOSE_DAYS
|
||||
|
||||
echo "清理完成"
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
name: Trivy 依赖漏洞扫描
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main', 'master']
|
||||
paths:
|
||||
- 'composer.lock'
|
||||
- 'package-lock.json'
|
||||
- 'yarn.lock'
|
||||
- 'pnpm-lock.yaml'
|
||||
pull_request:
|
||||
branches: ['main', 'master']
|
||||
# 每周一早上 8 点定时扫描
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
|
||||
jobs:
|
||||
trivy-scan:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: aquasec/trivy:latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://code.forgejo.org/actions/checkout@v4
|
||||
|
||||
- name: Run Trivy filesystem scan
|
||||
run: |
|
||||
trivy filesystem . --scanners vuln --severity HIGH,CRITICAL --exit-code 1 --format table --ignorefile .trivyignore 2>&1 || {
|
||||
echo
|
||||
echo ::warning::Trivy 发现了高危/严重漏洞,请检查上方输出。
|
||||
echo 如需忽略特定 CVE,请在仓库根目录创建 .trivyignore 文件。
|
||||
exit 1
|
||||
}
|
||||
echo ✅ Trivy 扫描通过,未发现高危漏洞。
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
name: WordPress 插件 CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main', 'master']
|
||||
pull_request:
|
||||
branches: ['main', 'master']
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
if: github.repository != 'WenPai-org/ci-workflows'
|
||||
runs-on: linux-arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: PHP Parallel Lint
|
||||
run: |
|
||||
parallel-lint --exclude vendor --exclude node_modules .
|
||||
|
||||
- name: PHPCS 代码规范检查
|
||||
run: |
|
||||
# 如果仓库有自定义 phpcs 配置则使用,否则用默认 WordPress 标准
|
||||
if [ -f phpcs.xml ] || [ -f phpcs.xml.dist ] || [ -f .phpcs.xml ] || [ -f .phpcs.xml.dist ]; then
|
||||
phpcs .
|
||||
else
|
||||
phpcs --standard=WordPress-Extra \
|
||||
--extensions=php \
|
||||
--ignore=vendor/*,node_modules/*,tests/*,lib/* \
|
||||
--report=full \
|
||||
-s .
|
||||
fi
|
||||
|
||||
- name: Gitleaks 密钥泄露扫描
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
|
||||
gitleaks detect --source=. --log-opts="$GITHUB_SHA~1..$GITHUB_SHA" --verbose --exit-code 1
|
||||
else
|
||||
gitleaks detect --source=. --verbose --exit-code 1
|
||||
fi
|
||||
echo "gitleaks 扫描通过"
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
name: WordPress 插件自动发布
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- '[0-9]+.[0-9]+*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: php:8.2-cli-alpine
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apk add --no-cache git curl zip unzip rsync npm composer python3
|
||||
|
||||
- name: Checkout
|
||||
uses: https://code.forgejo.org/actions/checkout@v4
|
||||
|
||||
- name: Install WP-CLI
|
||||
run: |
|
||||
curl -sO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
|
||||
chmod +x wp-cli.phar
|
||||
mv wp-cli.phar /usr/local/bin/wp
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
if [ -f composer.json ]; then
|
||||
composer install --no-dev --optimize-autoloader --no-interaction
|
||||
fi
|
||||
if [ -f package.json ]; then
|
||||
npm ci --production 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Run PHPCS (if configured)
|
||||
run: |
|
||||
if [ -f phpcs.xml ] || [ -f phpcs.xml.dist ] || [ -f .phpcs.xml ] || [ -f .phpcs.xml.dist ]; then
|
||||
composer lint 2>/dev/null || vendor/bin/phpcs . || {
|
||||
echo "::error::PHPCS 检查失败,请修复代码规范问题后重新打 tag。"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo "未找到 PHPCS 配置,跳过代码规范检查。"
|
||||
fi
|
||||
|
||||
- name: Generate i18n files
|
||||
run: |
|
||||
SLUG=$(basename $GITHUB_REPOSITORY)
|
||||
if [ -d languages ] || [ -d lang ]; then
|
||||
LANG_DIR=$([ -d languages ] && echo languages || echo lang)
|
||||
wp i18n make-pot . "$LANG_DIR/$SLUG.pot" --allow-root 2>/dev/null || true
|
||||
wp i18n make-json "$LANG_DIR/" --no-purge --allow-root 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Build release ZIP
|
||||
run: |
|
||||
PLUGIN_SLUG=$(basename $GITHUB_REPOSITORY)
|
||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||
mkdir -p /tmp/release
|
||||
rsync -a --exclude='.git' --exclude='.forgejo' --exclude='.github' \
|
||||
--exclude='node_modules' --exclude='.phpcs*' --exclude='phpstan*' \
|
||||
--exclude='tests' --exclude='.editorconfig' --exclude='.gitignore' \
|
||||
--exclude='phpunit*' --exclude='Gruntfile*' --exclude='webpack*' \
|
||||
--exclude='package.json' --exclude='package-lock.json' \
|
||||
--exclude='composer.lock' --exclude='.env*' \
|
||||
. /tmp/release/$PLUGIN_SLUG/
|
||||
cd /tmp/release
|
||||
zip -r /tmp/$PLUGIN_SLUG-$TAG_NAME.zip $PLUGIN_SLUG/
|
||||
echo "ZIP_PATH=/tmp/$PLUGIN_SLUG-$TAG_NAME.zip" >> $GITHUB_ENV
|
||||
echo "PLUGIN_SLUG=$PLUGIN_SLUG" >> $GITHUB_ENV
|
||||
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Forgejo Release
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
RELEASE_RESPONSE=$(curl -s -X POST \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases" \
|
||||
-d "{
|
||||
\"tag_name\": \"$TAG_NAME\",
|
||||
\"name\": \"$PLUGIN_SLUG $TAG_NAME\",
|
||||
\"body\": \"自动发布 $TAG_NAME\",
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}")
|
||||
RELEASE_ID=$(echo $RELEASE_RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "::warning::Release 创建失败或已存在。"
|
||||
exit 0
|
||||
fi
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-F "attachment=@$ZIP_PATH" \
|
||||
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=$PLUGIN_SLUG-$TAG_NAME.zip"
|
||||
echo "Release $TAG_NAME 发布成功!"
|
||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
vendor/
|
||||
node_modules/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
build/
|
||||
dist/
|
||||
|
||||
# AI & internal docs (sensitive)
|
||||
CLAUDE.md
|
||||
.agent/
|
||||
docs/
|
||||
204
CLAUDE.md
204
CLAUDE.md
|
|
@ -1,204 +0,0 @@
|
|||
# WPBridge - 文派云桥
|
||||
|
||||
WordPress 自定义源桥接插件,为开发者和高级用户提供灵活的更新源和 AI 服务桥接能力。
|
||||
|
||||
## 项目信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 插件名称 | WPBridge |
|
||||
| 中文名称 | 文派云桥 |
|
||||
| 版本 | 0.9.5 |
|
||||
| 开发目录 | `~/Projects/wpbridge/` |
|
||||
|
||||
## 核心定位
|
||||
|
||||
> **自定义源桥接器 - 让用户完全控制 WordPress 的外部连接**
|
||||
|
||||
### 目标 (Goals)
|
||||
|
||||
```
|
||||
WPBridge (文派云桥)
|
||||
├── 自托管插件/主题更新服务器桥接
|
||||
├── 商业插件自定义更新源管理
|
||||
├── AI 服务请求桥接(可选)
|
||||
└── 面向开发者/高级用户
|
||||
```
|
||||
|
||||
### 非目标 (Non-Goals)
|
||||
|
||||
> **明确边界,避免与文派生态其他产品重叠**
|
||||
|
||||
```
|
||||
WPBridge 不做:
|
||||
├── ❌ 官方源镜像/加速(文派叶子的职责)
|
||||
├── ❌ 商业插件破解或绕过授权
|
||||
├── ❌ AI 模型本体(WPMind 的职责)
|
||||
├── ❌ WordPress 核心汉化(LitePress 的职责)
|
||||
└── ❌ 镜像源基础设施(WPMirror 的职责)
|
||||
```
|
||||
|
||||
### 使用动机优先级
|
||||
|
||||
| 优先级 | 场景 | 用户类型 |
|
||||
|--------|------|----------|
|
||||
| P0 | 企业内网部署,私有仓库 | 企业用户 |
|
||||
| P1 | 商业插件更新源管理 | 商业插件用户 |
|
||||
| P2 | 开发测试,自托管服务器 | 开发者 |
|
||||
| P3 | AI 服务桥接 | AI 插件用户 |
|
||||
|
||||
```
|
||||
文派叶子 (WPCY) - 官方源加速
|
||||
├── WordPress.org → 文派镜像
|
||||
├── 开箱即用
|
||||
└── 面向普通用户
|
||||
|
||||
WPBridge (文派云桥) - 自定义源桥接
|
||||
├── 第三方自托管更新服务器
|
||||
├── 商业插件自定义更新源
|
||||
├── AI 服务桥接(可选)
|
||||
└── 面向开发者/高级用户
|
||||
```
|
||||
|
||||
## 与文派生态的关系
|
||||
|
||||
```
|
||||
文派生态 (WenPai.org)
|
||||
│
|
||||
├── 📦 WPMirror (wpmirror.com)
|
||||
│ └── 镜像源 - 插件/主题下载
|
||||
│
|
||||
├── 🇨🇳 LitePress (litepress.cn)
|
||||
│ └── WordPress 中国定制版
|
||||
│
|
||||
├── 🍃 文派叶子 WPCY (wpcy.com)
|
||||
│ ├── 中国源加速(官方源 → 文派镜像)
|
||||
│ ├── 插件/主题更新加速
|
||||
│ ├── 翻译下载优化
|
||||
│ └── 面向普通用户
|
||||
│
|
||||
├── 🤖 WPMind 文派心思
|
||||
│ └── 纯 AI 应用(国内 AI 服务)
|
||||
│
|
||||
├── 🏛️ ArkPress(文派开源自托管组件)
|
||||
│ ├── AspireCloud 中国分叉版本
|
||||
│ ├── 针对中国网络环境优化
|
||||
│ ├── 自托管更新服务器(服务端)
|
||||
│ └── 与 WPBridge 配合使用
|
||||
│
|
||||
└── 🌉 WPBridge 文派云桥 ← 本项目
|
||||
├── 自定义更新源桥接(客户端)
|
||||
├── 商业插件更新源
|
||||
├── AI 服务桥接(可选)
|
||||
└── 面向开发者/高级用户
|
||||
```
|
||||
|
||||
### ArkPress 与 WPBridge 的关系
|
||||
|
||||
```
|
||||
ArkPress(服务端) WPBridge(客户端)
|
||||
│ │
|
||||
│ 自托管更新服务器 │ 连接各种更新源
|
||||
│ AspireCloud 中国分叉 │ 统一管理界面
|
||||
│ 提供 API 服务 │ 性能优化
|
||||
│ │
|
||||
└────────── 配合使用 ──────────┘
|
||||
│
|
||||
▼
|
||||
完整的自托管更新解决方案
|
||||
```
|
||||
|
||||
## 目标用户
|
||||
|
||||
| 用户类型 | 需求 | WPBridge 价值 |
|
||||
|----------|------|---------------|
|
||||
| **企业用户** | 内网部署、私有仓库 | 配置内网更新服务器 |
|
||||
| **开发者** | 测试环境、自托管 | 灵活的更新源配置 |
|
||||
| **商业插件用户** | 多个商业插件管理 | 统一管理更新源 |
|
||||
| **AI 插件用户** | OpenAI 无法访问 | 透明切换国内服务 |
|
||||
|
||||
## 核心功能
|
||||
|
||||
```
|
||||
WPBridge (文派云桥)
|
||||
│
|
||||
├── 📦 更新源桥接
|
||||
│ ├── 自托管插件/主题更新服务器
|
||||
│ ├── 商业插件自定义更新源
|
||||
│ ├── 私有仓库支持 (GitHub/GitLab)
|
||||
│ └── 更新源管理界面
|
||||
│
|
||||
├── 🤖 AI 桥接
|
||||
│ ├── OpenAI API 兼容层
|
||||
│ ├── 商业插件 AI 适配器 (Yoast/Rank Math)
|
||||
│ └── 依赖 WPMind 提供 AI 能力(可选)
|
||||
│
|
||||
└── 🔧 高级配置
|
||||
├── 自定义 HTTP 头
|
||||
├── 认证方式配置 (API Key/OAuth/Basic)
|
||||
└── 代理设置
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
1. **企业内网部署**
|
||||
- 配置内网更新服务器
|
||||
- 私有插件/主题分发
|
||||
|
||||
2. **商业插件管理**
|
||||
- 统一管理多个商业插件的更新源
|
||||
- 解决授权验证超时问题
|
||||
|
||||
3. **开发测试**
|
||||
- 配置测试服务器进行插件开发
|
||||
- 版本控制和回滚
|
||||
|
||||
4. **AI 服务替换**
|
||||
- 将 OpenAI 请求转发到国内服务
|
||||
- 商业插件 AI 功能本地化
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [ROADMAP.md](ROADMAP.md) - 开发路线图
|
||||
- [DISCUSSION.md](DISCUSSION.md) - 讨论记录
|
||||
- [DESIGN.md](DESIGN.md) - 技术设计文档
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) - 业务流程与架构设计
|
||||
- [RESEARCH.md](RESEARCH.md) - 市场研究报告
|
||||
|
||||
## 技术栈
|
||||
|
||||
- PHP 7.4+
|
||||
- WordPress 5.9+
|
||||
- 可选依赖:WPMind(AI 桥接功能)
|
||||
|
||||
---
|
||||
|
||||
*创建日期: 2026-02-04*
|
||||
*最后更新: 2026-02-05*
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v0.9.5 (2026-02-05)
|
||||
- 代码评审修复:添加 AJAX 输入验证和清理
|
||||
- refresh_batch 方法添加数组键存在性检查
|
||||
- 移除 JS 中多余的参数传递
|
||||
|
||||
### v0.9.4 (2026-02-05)
|
||||
- 修复 WordPress.org 插件检测问题(启用 API 检查)
|
||||
- 实现异步批量检测,每批 5 个插件
|
||||
- 按钮显示进度(如 5/20),不再弹出多个通知
|
||||
- 新增 AJAX 端点:prepare_refresh、refresh_batch
|
||||
|
||||
### v0.9.2 (2026-02-05)
|
||||
- 将插件类型标签移到状态区域
|
||||
- 统一状态标签样式
|
||||
|
||||
### v0.9.1 (2026-02-05)
|
||||
- 代码评审修复:重复 CSS 定义、JS 语法错误
|
||||
|
||||
### v0.9.0 (2026-02-05)
|
||||
- 自定义模态框替换浏览器原生对话框
|
||||
- 添加 CSS 变量支持
|
||||
- 版本锁定、备份回滚功能
|
||||
- Site Health 集成
|
||||
- 更新日志聚合
|
||||
222
docs/API.md
222
docs/API.md
|
|
@ -1,222 +0,0 @@
|
|||
# WPBridge API 文档
|
||||
|
||||
> Bridge API - REST API 接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
WPBridge 提供 REST API 供外部系统调用,用于获取插件状态、管理更新源等。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础 URL**: `/wp-json/bridge/v1/`
|
||||
- **认证方式**: API Key
|
||||
- **响应格式**: JSON
|
||||
|
||||
## 认证
|
||||
|
||||
### 获取 API Key
|
||||
|
||||
1. 登录 WordPress 后台
|
||||
2. 进入「设置 > WPBridge > API」
|
||||
3. 点击「生成 API Key」
|
||||
4. 保存生成的 Key(只显示一次)
|
||||
|
||||
### 使用 API Key
|
||||
|
||||
在请求头中添加:
|
||||
|
||||
```http
|
||||
X-WPBridge-Key: your_api_key_here
|
||||
```
|
||||
|
||||
或使用查询参数:
|
||||
|
||||
```
|
||||
?api_key=your_api_key_here
|
||||
```
|
||||
|
||||
## API 端点
|
||||
|
||||
### 获取状态
|
||||
|
||||
获取 WPBridge 插件的运行状态。
|
||||
|
||||
```http
|
||||
GET /wp-json/bridge/v1/status
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"version": "0.8.0",
|
||||
"sources_count": 5,
|
||||
"enabled_sources": 4,
|
||||
"last_check": "2026-02-05 10:30:00",
|
||||
"cache_status": "healthy"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 获取更新源列表
|
||||
|
||||
获取所有配置的更新源。
|
||||
|
||||
```http
|
||||
GET /wp-json/bridge/v1/sources
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"sources": [
|
||||
{
|
||||
"id": "source_abc123",
|
||||
"name": "My Update Source",
|
||||
"type": "json",
|
||||
"enabled": true,
|
||||
"priority": 10,
|
||||
"last_check": "2026-02-05 10:00:00",
|
||||
"status": "healthy"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 检查更新源
|
||||
|
||||
检查指定更新源的连通性。
|
||||
|
||||
```http
|
||||
POST /wp-json/bridge/v1/sources/{source_id}/check
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"status": "healthy",
|
||||
"response_time": 0.234,
|
||||
"last_check": "2026-02-05 10:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 获取插件信息
|
||||
|
||||
获取指定插件的更新信息。
|
||||
|
||||
```http
|
||||
GET /wp-json/bridge/v1/plugins/{slug}/info
|
||||
```
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| slug | string | 插件 slug |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"name": "Example Plugin",
|
||||
"slug": "example-plugin",
|
||||
"version": "1.2.3",
|
||||
"requires": "5.9",
|
||||
"tested": "6.4",
|
||||
"download_url": "https://example.com/plugin.zip"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 获取主题信息
|
||||
|
||||
获取指定主题的更新信息。
|
||||
|
||||
```http
|
||||
GET /wp-json/bridge/v1/themes/{slug}/info
|
||||
```
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| slug | string | 主题 slug |
|
||||
|
||||
## 错误响应
|
||||
|
||||
### 错误格式
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": {
|
||||
"code": "error_code",
|
||||
"message": "错误描述"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误代码
|
||||
|
||||
| 代码 | HTTP 状态 | 说明 |
|
||||
|------|-----------|------|
|
||||
| `unauthorized` | 401 | 未提供或无效的 API Key |
|
||||
| `forbidden` | 403 | 权限不足 |
|
||||
| `not_found` | 404 | 资源不存在 |
|
||||
| `invalid_request` | 400 | 请求参数无效 |
|
||||
| `server_error` | 500 | 服务器内部错误 |
|
||||
|
||||
## 速率限制
|
||||
|
||||
- 默认限制:60 请求/分钟
|
||||
- 超出限制返回 429 状态码
|
||||
|
||||
## 示例代码
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
curl -X GET \
|
||||
'https://example.com/wp-json/bridge/v1/status' \
|
||||
-H 'X-WPBridge-Key: your_api_key'
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
$response = wp_remote_get( 'https://example.com/wp-json/bridge/v1/status', [
|
||||
'headers' => [
|
||||
'X-WPBridge-Key' => 'your_api_key',
|
||||
],
|
||||
] );
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
|
||||
```javascript
|
||||
fetch('https://example.com/wp-json/bridge/v1/status', {
|
||||
headers: {
|
||||
'X-WPBridge-Key': 'your_api_key'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => console.log(data));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-02-05*
|
||||
|
|
@ -1,349 +0,0 @@
|
|||
# 商业插件桥接方案技术评审报告
|
||||
|
||||
> 评审日期: 2026-02-15
|
||||
> 评审对象: COMMERCIAL-BRIDGE-SPEC.md v1.0.0-draft
|
||||
|
||||
---
|
||||
|
||||
## 评审摘要
|
||||
|
||||
| 级别 | 数量 | 说明 |
|
||||
|------|------|------|
|
||||
| High | 5 | 必须修复才能上线 |
|
||||
| Medium | 6 | 建议修复 |
|
||||
| Low | 4 | 可选优化 |
|
||||
|
||||
---
|
||||
|
||||
## High 级别问题
|
||||
|
||||
### H1: 站点 URL 哈希可被伪造
|
||||
|
||||
**位置**: `LicenseProxy::proxy_request()`, `Service::hashSiteURL()`
|
||||
|
||||
**问题**: 使用 `home_url()` 作为站点标识,攻击者可以:
|
||||
1. 在本地修改 `siteurl` 选项伪造任意站点
|
||||
2. 多个站点使用同一 API Key 绕过站点限制
|
||||
|
||||
**建议**:
|
||||
```php
|
||||
// 使用多因素站点指纹
|
||||
$site_fingerprint = hash('sha256', implode('|', [
|
||||
home_url(),
|
||||
DB_NAME,
|
||||
AUTH_KEY, // wp-config.php 中的密钥
|
||||
php_uname('n'), // 主机名
|
||||
]));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### H2: API Key 明文传输到日志
|
||||
|
||||
**位置**: `LicenseProxy::proxy_request()`
|
||||
|
||||
**问题**:
|
||||
```php
|
||||
Logger::debug('License proxy intercepting', [
|
||||
'vendor' => $vendor,
|
||||
'plugin' => $plugin_slug,
|
||||
'url' => $url, // 可能包含 license_key 参数
|
||||
]);
|
||||
```
|
||||
|
||||
原始 URL 可能包含用户的原厂 license_key,会被记录到日志。
|
||||
|
||||
**建议**:
|
||||
```php
|
||||
// 过滤敏感参数
|
||||
private function sanitize_url_for_log(string $url): string {
|
||||
return preg_replace(
|
||||
'/(license_key|license|key|password|secret)=[^&]+/i',
|
||||
'$1=[REDACTED]',
|
||||
$url
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### H3: 缺少请求签名验证
|
||||
|
||||
**位置**: 服务端 `HandleProxy()`
|
||||
|
||||
**问题**: 仅依赖 API Key 验证,缺少请求完整性校验,可能被中间人篡改。
|
||||
|
||||
**建议**:
|
||||
```go
|
||||
// 添加 HMAC 签名
|
||||
func (s *Service) verifyRequestSignature(apiKey string, req *ProxyRequest, signature string) bool {
|
||||
mac := hmac.New(sha256.New, []byte(apiKey))
|
||||
mac.Write([]byte(req.PluginSlug + req.SiteURL + req.Action))
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
return hmac.Equal([]byte(signature), []byte(expected))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### H4: 响应格式硬编码不完整
|
||||
|
||||
**位置**: `format_edd_response()`, `format_freemius_response()`
|
||||
|
||||
**问题**:
|
||||
1. EDD 响应缺少 `payment_id`, `customer_name`, `customer_email` 等字段
|
||||
2. Freemius 响应缺少 `secret_key`, `public_key` 等字段
|
||||
3. 某些插件会校验这些字段,导致授权失败
|
||||
|
||||
**建议**:
|
||||
- 需要逆向分析每个插件的实际校验逻辑
|
||||
- 建立插件响应格式数据库,动态匹配
|
||||
- 添加响应格式版本控制
|
||||
|
||||
---
|
||||
|
||||
### H5: 缺少 GPL 合规验证机制
|
||||
|
||||
**位置**: 整体架构
|
||||
|
||||
**问题**:
|
||||
1. 没有自动验证插件是否真的是 GPL 授权
|
||||
2. 依赖人工维护 `gpl_compatible` 字段
|
||||
3. 可能误桥接非 GPL 插件导致法律风险
|
||||
|
||||
**建议**:
|
||||
```php
|
||||
// 自动检测 GPL 兼容性
|
||||
class GPLValidator {
|
||||
public function validate(string $plugin_path): bool {
|
||||
// 1. 检查 license.txt
|
||||
// 2. 检查插件头部 License 字段
|
||||
// 3. 检查 readme.txt
|
||||
// 4. 查询 WordPress.org API
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Medium 级别问题
|
||||
|
||||
### M1: 缺少速率限制实现
|
||||
|
||||
**位置**: 服务端 API
|
||||
|
||||
**问题**: 文档提到"限流保护"但没有具体实现。
|
||||
|
||||
**建议**:
|
||||
```go
|
||||
// 使用令牌桶算法
|
||||
type RateLimiter struct {
|
||||
limits map[string]*rate.Limiter // 按 API Key
|
||||
}
|
||||
|
||||
func (r *RateLimiter) Allow(apiKey string) bool {
|
||||
limiter := r.getLimiter(apiKey)
|
||||
return limiter.Allow()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### M2: 缺少重试和熔断机制
|
||||
|
||||
**位置**: `LicenseProxy::proxy_request()`
|
||||
|
||||
**问题**: 代理请求失败时直接返回 false,没有重试逻辑。
|
||||
|
||||
**建议**:
|
||||
```php
|
||||
private function proxy_request_with_retry(...): array {
|
||||
$max_retries = 3;
|
||||
$backoff = 1;
|
||||
|
||||
for ($i = 0; $i < $max_retries; $i++) {
|
||||
$response = $this->do_proxy_request(...);
|
||||
if (!is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
sleep($backoff);
|
||||
$backoff *= 2;
|
||||
}
|
||||
|
||||
// 熔断:标记服务不可用
|
||||
$this->mark_service_unavailable();
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### M3: 数据库缺少软删除
|
||||
|
||||
**位置**: 数据库 Schema
|
||||
|
||||
**问题**: `site_activations` 使用 `ON DELETE CASCADE`,订阅删除时激活记录永久丢失。
|
||||
|
||||
**建议**:
|
||||
```sql
|
||||
ALTER TABLE site_activations ADD COLUMN deleted_at TIMESTAMP NULL;
|
||||
ALTER TABLE subscriptions ADD COLUMN deleted_at TIMESTAMP NULL;
|
||||
-- 使用软删除而非级联删除
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### M4: 缺少审计日志
|
||||
|
||||
**位置**: 整体架构
|
||||
|
||||
**问题**: `license_requests` 表只记录基本信息,缺少:
|
||||
- 请求 IP
|
||||
- User-Agent
|
||||
- 响应时间
|
||||
- 完整请求/响应内容(用于调试)
|
||||
|
||||
**建议**: 扩展日志表结构,添加详细审计字段。
|
||||
|
||||
---
|
||||
|
||||
### M5: BridgeManager 缺少构造函数
|
||||
|
||||
**位置**: `BridgeManager` 类
|
||||
|
||||
**问题**:
|
||||
```php
|
||||
class BridgeManager {
|
||||
private Settings $settings;
|
||||
private RemoteConfig $remote_config;
|
||||
// 缺少 __construct() 初始化这些属性
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### M6: 缺少插件版本兼容性检查
|
||||
|
||||
**位置**: 更新流程
|
||||
|
||||
**问题**: 桥接的插件版本可能与用户 WordPress/PHP 版本不兼容。
|
||||
|
||||
**建议**:
|
||||
```php
|
||||
public function check_compatibility(string $plugin_slug, string $version): array {
|
||||
$plugin = $this->registry->get($plugin_slug);
|
||||
$issues = [];
|
||||
|
||||
if (version_compare(get_bloginfo('version'), $plugin->min_wp_version, '<')) {
|
||||
$issues[] = 'WordPress 版本过低';
|
||||
}
|
||||
if (version_compare(PHP_VERSION, $plugin->min_php_version, '<')) {
|
||||
$issues[] = 'PHP 版本过低';
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Low 级别问题
|
||||
|
||||
### L1: 硬编码代理 URL
|
||||
|
||||
**位置**: `LicenseProxy::proxy_request()`
|
||||
|
||||
**问题**:
|
||||
```php
|
||||
$proxy_url = 'https://updates.wenpai.net/api/v1/license/proxy';
|
||||
```
|
||||
|
||||
**建议**: 使用配置项,支持自定义端点。
|
||||
|
||||
---
|
||||
|
||||
### L2: 缺少健康检查端点
|
||||
|
||||
**位置**: 服务端 API
|
||||
|
||||
**建议**: 添加 `GET /api/v1/health` 端点供客户端检测服务状态。
|
||||
|
||||
---
|
||||
|
||||
### L3: 响应缺少缓存控制
|
||||
|
||||
**位置**: 服务端响应
|
||||
|
||||
**建议**: 添加适当的 Cache-Control 头,减少重复请求。
|
||||
|
||||
---
|
||||
|
||||
### L4: 缺少国际化支持
|
||||
|
||||
**位置**: 错误消息
|
||||
|
||||
**问题**: Go 服务端错误消息是英文硬编码。
|
||||
|
||||
**建议**: 使用错误码,客户端根据错误码显示本地化消息。
|
||||
|
||||
---
|
||||
|
||||
## 商业风险评估
|
||||
|
||||
### 风险 1: 原厂法律行动 (高)
|
||||
|
||||
**分析**:
|
||||
- Elementor、Yoast 等公司有法务团队
|
||||
- 可能发送 DMCA 或律师函
|
||||
- GPL 不保护商标,使用插件名称可能侵权
|
||||
|
||||
**缓解**:
|
||||
1. 用户协议明确免责
|
||||
2. 不使用原厂商标/Logo
|
||||
3. 准备法律意见书
|
||||
|
||||
### 风险 2: 原厂技术对抗 (中)
|
||||
|
||||
**分析**:
|
||||
- 原厂可能更新授权 API 格式
|
||||
- 添加更复杂的校验机制
|
||||
- 检测并封禁桥接请求
|
||||
|
||||
**缓解**:
|
||||
1. 建立 API 变更监控
|
||||
2. 快速响应机制
|
||||
3. 多版本适配
|
||||
|
||||
### 风险 3: 用户信任问题 (中)
|
||||
|
||||
**分析**:
|
||||
- 用户可能担心安全性
|
||||
- 担心插件包被篡改
|
||||
- 担心服务稳定性
|
||||
|
||||
**缓解**:
|
||||
1. 透明的安全审计
|
||||
2. 提供校验和验证
|
||||
3. SLA 承诺
|
||||
|
||||
---
|
||||
|
||||
## 建议优先级
|
||||
|
||||
1. **立即修复**: H1, H2, H3 (安全相关)
|
||||
2. **上线前修复**: H4, H5, M1, M2
|
||||
3. **迭代优化**: M3-M6, L1-L4
|
||||
|
||||
---
|
||||
|
||||
## 结论
|
||||
|
||||
方案整体架构合理,但存在多个安全和实现细节问题需要解决。建议:
|
||||
|
||||
1. 先完成 High 级别问题修复
|
||||
2. 从 1-2 个简单插件(如 ACF Pro)开始试点
|
||||
3. 收集反馈后再扩展支持范围
|
||||
|
||||
---
|
||||
|
||||
*评审人: Claude Code*
|
||||
*评审日期: 2026-02-15*
|
||||
|
|
@ -1,814 +0,0 @@
|
|||
# 商业插件桥接技术规范
|
||||
|
||||
> 创建日期: 2026-02-15
|
||||
> 状态: 待评审
|
||||
> 版本: v1.0.0-draft
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
为购买了商业插件但授权过期/无法续费的用户提供替代更新源,实现:
|
||||
- 商业插件自动检测
|
||||
- 授权验证代理
|
||||
- 更新包下载桥接
|
||||
- 订阅管理
|
||||
|
||||
### 1.2 核心原则
|
||||
|
||||
- **GPL 合规**: 只桥接 GPL 授权的插件
|
||||
- **透明代理**: 不修改插件代码,只代理网络请求
|
||||
- **用户自主**: 用户明确选择启用桥接
|
||||
|
||||
---
|
||||
|
||||
## 2. 系统架构
|
||||
|
||||
### 2.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 用户 WordPress 站点 │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ CommercialDetector│ │ LicenseProxy │ │ UpdateBridge │ │
|
||||
│ │ (已有) │ │ (新增) │ │ (已有扩展) │ │
|
||||
│ └────────┬─────────┘ └────────┬────────┘ └────────┬────────┘ │
|
||||
└───────────┼─────────────────────┼─────────────────────┼──────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 文派云桥服务端 (wenpai-bridge) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ Plugin Registry │ │ License Service │ │ CDN / Storage │ │
|
||||
│ │ 插件注册表 │ │ 授权服务 │ │ 下载存储 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 数据流
|
||||
|
||||
```
|
||||
1. 检测流程:
|
||||
用户站点 → CommercialDetector → 识别商业插件 → 显示桥接选项
|
||||
|
||||
2. 授权流程:
|
||||
商业插件授权请求 → LicenseProxy 拦截 → 文派授权服务 → 返回有效授权
|
||||
|
||||
3. 更新流程:
|
||||
WordPress 更新检查 → UpdateBridge → wenpai-bridge API → 返回更新信息
|
||||
|
||||
4. 下载流程:
|
||||
用户点击更新 → 下载请求 → 文派 CDN → 返回插件包
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 客户端组件设计
|
||||
|
||||
### 3.1 LicenseProxy (新增)
|
||||
|
||||
#### 3.1.1 职责
|
||||
- 拦截商业插件的授权验证 HTTP 请求
|
||||
- 识别授权系统类型 (EDD, Freemius, WC_AM 等)
|
||||
- 转发到文派授权代理服务
|
||||
- 转换响应格式以匹配原厂 API
|
||||
|
||||
#### 3.1.2 类设计
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WPBridge\Commercial;
|
||||
|
||||
use WPBridge\Core\Logger;
|
||||
use WPBridge\Core\Settings;
|
||||
|
||||
class LicenseProxy {
|
||||
/**
|
||||
* 支持的授权系统配置
|
||||
*/
|
||||
private const VENDORS = [
|
||||
'edd' => [
|
||||
'name' => 'EDD Software Licensing',
|
||||
'patterns' => [
|
||||
'/edd-sl/',
|
||||
'/edd-api/',
|
||||
'action=activate_license',
|
||||
'action=check_license',
|
||||
'action=deactivate_license',
|
||||
],
|
||||
'response_format' => 'edd',
|
||||
],
|
||||
'freemius' => [
|
||||
'name' => 'Freemius',
|
||||
'patterns' => [
|
||||
'api.freemius.com',
|
||||
'wp-json/freemius',
|
||||
],
|
||||
'response_format' => 'freemius',
|
||||
],
|
||||
'wc_am' => [
|
||||
'name' => 'WooCommerce API Manager',
|
||||
'patterns' => [
|
||||
'wc-api/wc-am-api',
|
||||
'wc-api/am-software-api',
|
||||
],
|
||||
'response_format' => 'wc_am',
|
||||
],
|
||||
'envato' => [
|
||||
'name' => 'Envato Market',
|
||||
'patterns' => [
|
||||
'api.envato.com',
|
||||
],
|
||||
'response_format' => 'envato',
|
||||
],
|
||||
];
|
||||
|
||||
private Settings $settings;
|
||||
private array $bridged_plugins = [];
|
||||
|
||||
public function __construct(Settings $settings) {
|
||||
$this->settings = $settings;
|
||||
$this->bridged_plugins = $this->settings->get('bridged_plugins', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
public function init(): void {
|
||||
if (!$this->is_enabled()) {
|
||||
return;
|
||||
}
|
||||
add_filter('pre_http_request', [$this, 'intercept_request'], 5, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用
|
||||
*/
|
||||
private function is_enabled(): bool {
|
||||
return (bool) $this->settings->get('license_proxy_enabled', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截 HTTP 请求
|
||||
*/
|
||||
public function intercept_request($preempt, array $args, string $url) {
|
||||
// 1. 检测授权系统
|
||||
$vendor = $this->detect_vendor($url);
|
||||
if ($vendor === null) {
|
||||
return $preempt;
|
||||
}
|
||||
|
||||
// 2. 提取插件标识
|
||||
$plugin_slug = $this->extract_plugin_slug($url, $args, $vendor);
|
||||
if ($plugin_slug === null) {
|
||||
return $preempt;
|
||||
}
|
||||
|
||||
// 3. 检查是否在桥接列表
|
||||
if (!$this->is_bridged($plugin_slug)) {
|
||||
return $preempt;
|
||||
}
|
||||
|
||||
Logger::debug('License proxy intercepting', [
|
||||
'vendor' => $vendor,
|
||||
'plugin' => $plugin_slug,
|
||||
'url' => $url,
|
||||
]);
|
||||
|
||||
// 4. 代理到文派服务
|
||||
return $this->proxy_request($vendor, $plugin_slug, $url, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测授权系统供应商
|
||||
*/
|
||||
private function detect_vendor(string $url): ?string {
|
||||
foreach (self::VENDORS as $vendor_key => $config) {
|
||||
foreach ($config['patterns'] as $pattern) {
|
||||
if (stripos($url, $pattern) !== false) {
|
||||
return $vendor_key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取插件 slug
|
||||
*/
|
||||
private function extract_plugin_slug(string $url, array $args, string $vendor): ?string {
|
||||
// 从 URL 参数提取
|
||||
$parsed = wp_parse_url($url);
|
||||
if (isset($parsed['query'])) {
|
||||
parse_str($parsed['query'], $query);
|
||||
|
||||
// EDD 格式
|
||||
if (isset($query['item_name'])) {
|
||||
return sanitize_title($query['item_name']);
|
||||
}
|
||||
if (isset($query['item_id'])) {
|
||||
return $this->resolve_item_id($query['item_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// 从 POST body 提取
|
||||
if (isset($args['body']) && is_array($args['body'])) {
|
||||
if (isset($args['body']['item_name'])) {
|
||||
return sanitize_title($args['body']['item_name']);
|
||||
}
|
||||
if (isset($args['body']['product_id'])) {
|
||||
return $this->resolve_item_id($args['body']['product_id']);
|
||||
}
|
||||
}
|
||||
|
||||
// Freemius 格式: /v1/plugins/{id}/...
|
||||
if ($vendor === 'freemius' && preg_match('#/plugins/(\d+)/#', $url, $matches)) {
|
||||
return $this->resolve_freemius_id($matches[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件是否在桥接列表
|
||||
*/
|
||||
private function is_bridged(string $plugin_slug): bool {
|
||||
return in_array($plugin_slug, $this->bridged_plugins, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理请求到文派服务
|
||||
*/
|
||||
private function proxy_request(string $vendor, string $plugin_slug, string $original_url, array $args): array {
|
||||
$proxy_url = 'https://updates.wenpai.net/api/v1/license/proxy';
|
||||
|
||||
$response = wp_remote_post($proxy_url, [
|
||||
'timeout' => 15,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-WPBridge-Key' => $this->get_api_key(),
|
||||
'X-WPBridge-Site' => home_url(),
|
||||
],
|
||||
'body' => wp_json_encode([
|
||||
'vendor' => $vendor,
|
||||
'plugin_slug' => $plugin_slug,
|
||||
'original_url' => $original_url,
|
||||
'action' => $this->extract_action($original_url, $args),
|
||||
'site_url' => home_url(),
|
||||
]),
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
Logger::error('License proxy failed', [
|
||||
'error' => $response->get_error_message(),
|
||||
]);
|
||||
// 失败时不拦截,让原始请求继续
|
||||
return false;
|
||||
}
|
||||
|
||||
// 转换响应格式
|
||||
return $this->transform_response($vendor, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换响应格式以匹配原厂 API
|
||||
*/
|
||||
private function transform_response(string $vendor, array $response): array {
|
||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if (!isset($body['success']) || !$body['success']) {
|
||||
return false; // 让原始请求继续
|
||||
}
|
||||
|
||||
$license = $body['license'] ?? [];
|
||||
|
||||
// 根据不同授权系统返回不同格式
|
||||
switch ($vendor) {
|
||||
case 'edd':
|
||||
return $this->format_edd_response($license);
|
||||
case 'freemius':
|
||||
return $this->format_freemius_response($license);
|
||||
case 'wc_am':
|
||||
return $this->format_wc_am_response($license);
|
||||
default:
|
||||
return $this->format_generic_response($license);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 EDD 响应
|
||||
*/
|
||||
private function format_edd_response(array $license): array {
|
||||
$body = wp_json_encode([
|
||||
'success' => true,
|
||||
'license' => $license['status'] ?? 'valid',
|
||||
'item_name' => $license['item_name'] ?? '',
|
||||
'expires' => $license['expires'] ?? 'lifetime',
|
||||
'license_limit' => $license['license_limit'] ?? 0,
|
||||
'site_count' => $license['site_count'] ?? 1,
|
||||
'activations_left' => $license['activations_left'] ?? 'unlimited',
|
||||
'checksum' => $license['checksum'] ?? '',
|
||||
]);
|
||||
|
||||
return [
|
||||
'response' => ['code' => 200, 'message' => 'OK'],
|
||||
'body' => $body,
|
||||
'headers' => ['content-type' => 'application/json'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 Freemius 响应
|
||||
*/
|
||||
private function format_freemius_response(array $license): array {
|
||||
$body = wp_json_encode([
|
||||
'id' => $license['id'] ?? 0,
|
||||
'plugin_id' => $license['plugin_id'] ?? 0,
|
||||
'user_id' => $license['user_id'] ?? 0,
|
||||
'plan_id' => $license['plan_id'] ?? 0,
|
||||
'pricing_id' => $license['pricing_id'] ?? 0,
|
||||
'quota' => $license['license_limit'] ?? null,
|
||||
'activated' => $license['site_count'] ?? 1,
|
||||
'activated_local' => 1,
|
||||
'expiration' => $license['expires'] ?? null,
|
||||
'is_free_localhost' => false,
|
||||
'is_block_features' => false,
|
||||
'is_cancelled' => false,
|
||||
]);
|
||||
|
||||
return [
|
||||
'response' => ['code' => 200, 'message' => 'OK'],
|
||||
'body' => $body,
|
||||
'headers' => ['content-type' => 'application/json'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 WC API Manager 响应
|
||||
*/
|
||||
private function format_wc_am_response(array $license): array {
|
||||
$body = wp_json_encode([
|
||||
'success' => true,
|
||||
'status_check' => 'active',
|
||||
'activations' => (string) ($license['site_count'] ?? 1),
|
||||
'activations_limit' => (string) ($license['license_limit'] ?? 'unlimited'),
|
||||
]);
|
||||
|
||||
return [
|
||||
'response' => ['code' => 200, 'message' => 'OK'],
|
||||
'body' => $body,
|
||||
'headers' => ['content-type' => 'application/json'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 API Key
|
||||
*/
|
||||
private function get_api_key(): string {
|
||||
return $this->settings->get('wenpai_api_key', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取操作类型
|
||||
*/
|
||||
private function extract_action(string $url, array $args): string {
|
||||
// 从 URL 提取
|
||||
if (preg_match('/action=(\w+)/', $url, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// 从 body 提取
|
||||
if (isset($args['body']['edd_action'])) {
|
||||
return $args['body']['edd_action'];
|
||||
}
|
||||
if (isset($args['body']['wc-api'])) {
|
||||
return $args['body']['request'] ?? 'status';
|
||||
}
|
||||
|
||||
return 'check_license';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 BridgeManager (新增)
|
||||
|
||||
#### 3.2.1 职责
|
||||
- 管理桥接插件列表
|
||||
- 提供桥接启用/禁用 UI
|
||||
- 与服务端同步可桥接插件列表
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WPBridge\Commercial;
|
||||
|
||||
use WPBridge\Core\Settings;
|
||||
use WPBridge\Core\RemoteConfig;
|
||||
|
||||
class BridgeManager {
|
||||
private Settings $settings;
|
||||
private RemoteConfig $remote_config;
|
||||
|
||||
/**
|
||||
* 获取可桥接的商业插件列表(从服务端)
|
||||
*/
|
||||
public function get_available_plugins(): array {
|
||||
return $this->remote_config->get('bridgeable_plugins', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已启用桥接的插件
|
||||
*/
|
||||
public function get_bridged_plugins(): array {
|
||||
return $this->settings->get('bridged_plugins', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用插件桥接
|
||||
*/
|
||||
public function enable_bridge(string $plugin_slug): bool {
|
||||
// 检查是否在可桥接列表
|
||||
$available = $this->get_available_plugins();
|
||||
if (!isset($available[$plugin_slug])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查订阅限制
|
||||
if (!$this->check_subscription_limit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bridged = $this->get_bridged_plugins();
|
||||
if (!in_array($plugin_slug, $bridged, true)) {
|
||||
$bridged[] = $plugin_slug;
|
||||
$this->settings->set('bridged_plugins', $bridged);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用插件桥接
|
||||
*/
|
||||
public function disable_bridge(string $plugin_slug): bool {
|
||||
$bridged = $this->get_bridged_plugins();
|
||||
$bridged = array_diff($bridged, [$plugin_slug]);
|
||||
return $this->settings->set('bridged_plugins', array_values($bridged));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查订阅限制
|
||||
*/
|
||||
private function check_subscription_limit(): bool {
|
||||
$subscription = $this->get_subscription();
|
||||
if ($subscription['plan'] === 'agency') {
|
||||
return true; // 无限制
|
||||
}
|
||||
|
||||
$current_count = count($this->get_bridged_plugins());
|
||||
$limit = $subscription['plugins_limit'] ?? 5;
|
||||
|
||||
return $current_count < $limit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 服务端组件设计
|
||||
|
||||
### 4.1 License Service (wenpai-bridge 扩展)
|
||||
|
||||
#### 4.1.1 API 端点
|
||||
|
||||
```
|
||||
POST /api/v1/license/proxy
|
||||
- 授权代理请求
|
||||
|
||||
GET /api/v1/license/status
|
||||
- 查询授权状态
|
||||
|
||||
POST /api/v1/license/activate
|
||||
- 激活站点
|
||||
|
||||
POST /api/v1/license/deactivate
|
||||
- 停用站点
|
||||
```
|
||||
|
||||
#### 4.1.2 Go 实现
|
||||
|
||||
```go
|
||||
// internal/license/service.go
|
||||
package license
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidAPIKey = errors.New("invalid api key")
|
||||
ErrPluginNotBridged = errors.New("plugin not in bridge list")
|
||||
ErrSubscriptionRequired = errors.New("subscription required")
|
||||
ErrSiteLimitExceeded = errors.New("site activation limit exceeded")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *Database
|
||||
subscriptions *SubscriptionService
|
||||
registry *PluginRegistry
|
||||
}
|
||||
|
||||
type ProxyRequest struct {
|
||||
Vendor string `json:"vendor"`
|
||||
PluginSlug string `json:"plugin_slug"`
|
||||
OriginalURL string `json:"original_url"`
|
||||
Action string `json:"action"`
|
||||
SiteURL string `json:"site_url"`
|
||||
}
|
||||
|
||||
type LicenseResponse struct {
|
||||
Success bool `json:"success"`
|
||||
License *LicenseInfo `json:"license,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type LicenseInfo struct {
|
||||
Status string `json:"status"`
|
||||
Expires string `json:"expires"`
|
||||
LicenseLimit int `json:"license_limit"`
|
||||
SiteCount int `json:"site_count"`
|
||||
ActivationsLeft string `json:"activations_left"`
|
||||
Features []string `json:"features"`
|
||||
ItemName string `json:"item_name,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Service) HandleProxy(ctx context.Context, apiKey string, req *ProxyRequest) (*LicenseResponse, error) {
|
||||
// 1. 验证 API Key
|
||||
subscription, err := s.subscriptions.GetByAPIKey(ctx, apiKey)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidAPIKey
|
||||
}
|
||||
|
||||
// 2. 检查订阅状态
|
||||
if !subscription.IsActive() {
|
||||
return nil, ErrSubscriptionRequired
|
||||
}
|
||||
|
||||
// 3. 检查插件是否可桥接
|
||||
plugin, err := s.registry.GetPlugin(ctx, req.PluginSlug)
|
||||
if err != nil || !plugin.BridgeEnabled {
|
||||
return nil, ErrPluginNotBridged
|
||||
}
|
||||
|
||||
// 4. 检查站点激活限制
|
||||
siteHash := s.hashSiteURL(req.SiteURL)
|
||||
if !s.checkSiteLimit(ctx, subscription, siteHash) {
|
||||
return nil, ErrSiteLimitExceeded
|
||||
}
|
||||
|
||||
// 5. 记录/更新站点激活
|
||||
s.recordActivation(ctx, subscription.ID, req.SiteURL, siteHash)
|
||||
|
||||
// 6. 构建响应
|
||||
license := &LicenseInfo{
|
||||
Status: "valid",
|
||||
Expires: subscription.ExpiresAt.Format("2006-01-02"),
|
||||
LicenseLimit: subscription.SiteLimit,
|
||||
SiteCount: s.getSiteCount(ctx, subscription.ID),
|
||||
ActivationsLeft: s.getActivationsLeft(subscription),
|
||||
Features: []string{"updates"},
|
||||
ItemName: plugin.Name,
|
||||
Checksum: s.generateChecksum(subscription, plugin),
|
||||
}
|
||||
|
||||
return &LicenseResponse{
|
||||
Success: true,
|
||||
License: license,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) hashSiteURL(url string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(url))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (s *Service) checkSiteLimit(ctx context.Context, sub *Subscription, siteHash string) bool {
|
||||
// Agency 计划无限制
|
||||
if sub.Plan == "agency" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否已激活此站点
|
||||
exists, _ := s.db.SiteActivationExists(ctx, sub.ID, siteHash)
|
||||
if exists {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否超过限制
|
||||
count := s.getSiteCount(ctx, sub.ID)
|
||||
return count < sub.SiteLimit
|
||||
}
|
||||
|
||||
func (s *Service) recordActivation(ctx context.Context, subID int64, siteURL, siteHash string) {
|
||||
s.db.UpsertSiteActivation(ctx, &SiteActivation{
|
||||
SubscriptionID: subID,
|
||||
SiteURL: siteURL,
|
||||
SiteHash: siteHash,
|
||||
LastSeen: time.Now(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 数据库 Schema
|
||||
|
||||
```sql
|
||||
-- 可桥接插件注册表
|
||||
CREATE TABLE bridgeable_plugins (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(255) NOT NULL UNIQUE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
vendor VARCHAR(50) NOT NULL,
|
||||
vendor_url VARCHAR(500),
|
||||
gpl_compatible BOOLEAN DEFAULT TRUE,
|
||||
bridge_enabled BOOLEAN DEFAULT TRUE,
|
||||
download_url VARCHAR(500),
|
||||
latest_version VARCHAR(50),
|
||||
min_wp_version VARCHAR(20),
|
||||
tested_wp_version VARCHAR(20),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_vendor (vendor),
|
||||
INDEX idx_enabled (bridge_enabled)
|
||||
);
|
||||
|
||||
-- 用户订阅
|
||||
CREATE TABLE subscriptions (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
api_key VARCHAR(64) NOT NULL UNIQUE,
|
||||
plan ENUM('free', 'pro', 'agency') DEFAULT 'free',
|
||||
site_limit INT DEFAULT 1,
|
||||
plugins_limit INT DEFAULT 0,
|
||||
status ENUM('active', 'expired', 'cancelled') DEFAULT 'active',
|
||||
expires_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_api_key (api_key),
|
||||
INDEX idx_status (status)
|
||||
);
|
||||
|
||||
-- 站点激活记录
|
||||
CREATE TABLE site_activations (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
subscription_id BIGINT UNSIGNED NOT NULL,
|
||||
site_url VARCHAR(500) NOT NULL,
|
||||
site_hash VARCHAR(64) NOT NULL,
|
||||
activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_sub_site (subscription_id, site_hash),
|
||||
INDEX idx_subscription (subscription_id),
|
||||
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 授权请求日志
|
||||
CREATE TABLE license_requests (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
subscription_id BIGINT UNSIGNED,
|
||||
plugin_slug VARCHAR(255) NOT NULL,
|
||||
vendor VARCHAR(50) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
site_url VARCHAR(500),
|
||||
success BOOLEAN DEFAULT TRUE,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_subscription (subscription_id),
|
||||
INDEX idx_plugin (plugin_slug),
|
||||
INDEX idx_created (created_at)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 支持的商业插件
|
||||
|
||||
### 5.1 第一批支持 (P0)
|
||||
|
||||
| 插件 | Slug | 授权系统 | GPL | 状态 |
|
||||
|------|------|----------|-----|------|
|
||||
| Elementor Pro | elementor-pro | EDD | Yes | 待添加 |
|
||||
| Yoast SEO Premium | wordpress-seo-premium | EDD | Yes | 待添加 |
|
||||
| ACF Pro | advanced-custom-fields-pro | EDD | Yes | 待添加 |
|
||||
| Gravity Forms | gravityforms | EDD | Yes | 待添加 |
|
||||
| WPForms Pro | wpforms | EDD | Yes | 待添加 |
|
||||
|
||||
### 5.2 第二批支持 (P1)
|
||||
|
||||
| 插件 | Slug | 授权系统 | GPL | 状态 |
|
||||
|------|------|----------|-----|------|
|
||||
| Rank Math Pro | seo-by-rank-math-pro | Freemius | Yes | 待添加 |
|
||||
| WP Rocket | wp-rocket | Custom | Yes | 待添加 |
|
||||
| Perfmatters | perfmatters | EDD | Yes | 待添加 |
|
||||
| FlyingPress | flavor | EDD | Yes | 待添加 |
|
||||
|
||||
### 5.3 不支持的插件
|
||||
|
||||
以下插件因非 GPL 或其他原因不支持:
|
||||
- Envato 独占插件(非 GPL)
|
||||
- 包含 SaaS 依赖的插件(如 Jetpack Premium)
|
||||
|
||||
---
|
||||
|
||||
## 6. 安全考虑
|
||||
|
||||
### 6.1 API Key 安全
|
||||
- API Key 使用 AES-256 加密存储
|
||||
- 传输使用 HTTPS
|
||||
- 支持 Key 轮换
|
||||
|
||||
### 6.2 请求验证
|
||||
- 验证请求来源站点
|
||||
- 防止重放攻击(nonce)
|
||||
- 限流保护
|
||||
|
||||
### 6.3 下载安全
|
||||
- 所有下载包经过病毒扫描
|
||||
- 提供 SHA256 校验和
|
||||
- 支持签名验证
|
||||
|
||||
---
|
||||
|
||||
## 7. 商业模式
|
||||
|
||||
### 7.1 定价
|
||||
|
||||
| 计划 | 价格 | 站点数 | 插件数 | 功能 |
|
||||
|------|------|--------|--------|------|
|
||||
| Free | ¥0 | 1 | 0 | 检测、开源更新 |
|
||||
| Pro | ¥199/年 | 3 | 5 | 商业插件桥接 |
|
||||
| Agency | ¥999/年 | 无限 | 无限 | 全功能 + 优先支持 |
|
||||
|
||||
### 7.2 收入预测
|
||||
|
||||
假设:
|
||||
- 第一年 1000 付费用户
|
||||
- Pro:Agency = 7:3
|
||||
- 续费率 60%
|
||||
|
||||
年收入 = 700 × 199 + 300 × 999 = ¥439,000
|
||||
|
||||
---
|
||||
|
||||
## 8. 实施计划
|
||||
|
||||
### Phase 1: 基础设施 (2周)
|
||||
- [ ] LicenseProxy 客户端组件
|
||||
- [ ] License Service 服务端
|
||||
- [ ] 数据库 Schema
|
||||
- [ ] 基础 API
|
||||
|
||||
### Phase 2: 插件支持 (2周)
|
||||
- [ ] EDD 授权系统适配
|
||||
- [ ] 第一批 5 个插件接入
|
||||
- [ ] 下载 CDN 配置
|
||||
|
||||
### Phase 3: 订阅系统 (1周)
|
||||
- [ ] 订阅管理 UI
|
||||
- [ ] 支付集成
|
||||
- [ ] API Key 管理
|
||||
|
||||
### Phase 4: 测试发布 (1周)
|
||||
- [ ] 集成测试
|
||||
- [ ] Beta 测试
|
||||
- [ ] 文档完善
|
||||
- [ ] 正式发布
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与缓解
|
||||
|
||||
### 9.1 法律风险
|
||||
- **风险**: 原厂法律诉讼
|
||||
- **缓解**: 只支持 GPL 插件,明确用户协议
|
||||
|
||||
### 9.2 技术风险
|
||||
- **风险**: 原厂 API 变更
|
||||
- **缓解**: 模块化设计,快速适配
|
||||
|
||||
### 9.3 运营风险
|
||||
- **风险**: 原厂封禁
|
||||
- **缓解**: 自建 CDN,多源备份
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-02-15*
|
||||
202
docs/FAQ.md
202
docs/FAQ.md
|
|
@ -1,202 +0,0 @@
|
|||
# WPBridge 常见问题
|
||||
|
||||
> 常见问题解答 (FAQ)
|
||||
|
||||
## 安装与配置
|
||||
|
||||
### Q: WPBridge 的系统要求是什么?
|
||||
|
||||
- WordPress 5.9 或更高版本
|
||||
- PHP 7.4 或更高版本
|
||||
- 建议启用 cURL 扩展
|
||||
|
||||
### Q: 如何安装 WPBridge?
|
||||
|
||||
1. 下载插件 ZIP 文件
|
||||
2. 在 WordPress 后台进入「插件 > 安装插件 > 上传插件」
|
||||
3. 上传并激活插件
|
||||
4. 进入「设置 > WPBridge」配置
|
||||
|
||||
### Q: WPBridge 与文派叶子(WPCY)有什么区别?
|
||||
|
||||
| 功能 | 文派叶子 (WPCY) | WPBridge |
|
||||
|------|-----------------|----------|
|
||||
| 目标用户 | 普通用户 | 开发者/高级用户 |
|
||||
| 主要功能 | 官方源加速 | 自定义源桥接 |
|
||||
| 配置复杂度 | 开箱即用 | 需要配置 |
|
||||
| 商业插件支持 | 有限 | 完整支持 |
|
||||
|
||||
两者可以同时使用,WPBridge 会自动检测 WPCY 并协同工作。
|
||||
|
||||
---
|
||||
|
||||
## 更新源
|
||||
|
||||
### Q: 支持哪些类型的更新源?
|
||||
|
||||
- JSON API(标准格式)
|
||||
- GitHub Releases
|
||||
- GitLab Releases
|
||||
- Gitee Releases
|
||||
- ArkPress(文派自托管)
|
||||
- AspireCloud
|
||||
- Plugin Update Checker (PUC) 格式
|
||||
- FAIR Package Manager
|
||||
|
||||
### Q: 如何添加 GitHub 仓库作为更新源?
|
||||
|
||||
1. 进入「更新源」标签
|
||||
2. 点击「添加更新源」
|
||||
3. 选择类型为「GitHub」
|
||||
4. 填写仓库 URL,如 `https://github.com/owner/repo`
|
||||
5. 如果是私有仓库,填写 Personal Access Token
|
||||
6. 保存
|
||||
|
||||
### Q: 更新源不工作怎么办?
|
||||
|
||||
1. **检查 URL**:确保 URL 格式正确
|
||||
2. **测试连通性**:在「诊断」页面点击「测试」按钮
|
||||
3. **检查认证**:私有源需要正确的 Token
|
||||
4. **查看日志**:启用调试模式查看详细日志
|
||||
5. **检查防火墙**:确保服务器可以访问更新源
|
||||
|
||||
### Q: 多个更新源提供同一插件时如何处理?
|
||||
|
||||
WPBridge 会:
|
||||
1. 按优先级排序(数字越小优先级越高)
|
||||
2. 比较版本号,选择最高版本
|
||||
3. 如果版本相同,使用优先级最高的源
|
||||
|
||||
---
|
||||
|
||||
## 商业插件
|
||||
|
||||
### Q: 如何管理商业插件的更新?
|
||||
|
||||
1. WPBridge 会自动检测商业插件
|
||||
2. 您可以为商业插件配置专用更新源
|
||||
3. 或者使用插件原有的更新机制
|
||||
|
||||
### Q: 商业插件检测不准确怎么办?
|
||||
|
||||
您可以手动标记插件类型:
|
||||
1. 在「概览」页面找到插件
|
||||
2. 点击类型标签
|
||||
3. 选择正确的类型(免费/商业/第三方)
|
||||
|
||||
### Q: WPBridge 会绕过商业插件的授权验证吗?
|
||||
|
||||
**不会**。WPBridge 只是提供更新源桥接功能,不会绕过任何授权验证。您仍需要有效的授权才能使用商业插件。
|
||||
|
||||
---
|
||||
|
||||
## 性能与缓存
|
||||
|
||||
### Q: WPBridge 会影响网站性能吗?
|
||||
|
||||
WPBridge 设计时考虑了性能:
|
||||
- 使用缓存减少请求次数
|
||||
- 支持并行请求
|
||||
- 后台预热机制
|
||||
- 失败源冷却机制
|
||||
|
||||
### Q: 如何清除缓存?
|
||||
|
||||
**方法一:管理界面**
|
||||
1. 进入「诊断」标签
|
||||
2. 点击「清除缓存」按钮
|
||||
|
||||
**方法二:WP-CLI**
|
||||
```bash
|
||||
wp bridge cache clear
|
||||
```
|
||||
|
||||
### Q: 缓存时间可以调整吗?
|
||||
|
||||
可以。在「设置」标签中可以调整缓存时间:
|
||||
- 1 小时
|
||||
- 6 小时
|
||||
- 12 小时(默认)
|
||||
- 24 小时
|
||||
|
||||
---
|
||||
|
||||
## API 与集成
|
||||
|
||||
### Q: 如何使用 Bridge API?
|
||||
|
||||
1. 在「API」标签生成 API Key
|
||||
2. 在请求头中添加 `X-WPBridge-Key: your_key`
|
||||
3. 调用 `/wp-json/bridge/v1/` 下的端点
|
||||
|
||||
详见 [API 文档](API.md)。
|
||||
|
||||
### Q: API Key 丢失了怎么办?
|
||||
|
||||
API Key 只在生成时显示一次。如果丢失:
|
||||
1. 撤销旧的 Key
|
||||
2. 生成新的 Key
|
||||
|
||||
### Q: 可以与其他系统集成吗?
|
||||
|
||||
可以。WPBridge 提供:
|
||||
- REST API 供外部调用
|
||||
- WP-CLI 命令供脚本使用
|
||||
- Webhook 通知功能
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Q: 插件激活后没有菜单?
|
||||
|
||||
1. 检查是否有 PHP 错误
|
||||
2. 尝试停用其他插件排查冲突
|
||||
3. 检查用户权限(需要管理员权限)
|
||||
|
||||
### Q: 更新检查失败?
|
||||
|
||||
1. 检查网络连接
|
||||
2. 检查更新源 URL 是否可访问
|
||||
3. 查看「诊断」页面的错误信息
|
||||
4. 启用调试模式查看详细日志
|
||||
|
||||
### Q: 配置丢失了怎么恢复?
|
||||
|
||||
如果有备份:
|
||||
1. 进入「设置」标签
|
||||
2. 点击「导入」按钮
|
||||
3. 选择备份的 JSON 文件
|
||||
|
||||
如果没有备份,需要重新配置。建议定期导出配置作为备份。
|
||||
|
||||
### Q: 如何获取调试信息?
|
||||
|
||||
1. 在「设置」中启用「调试模式」
|
||||
2. 在「日志」标签查看日志
|
||||
3. 或使用「诊断」页面的「导出报告」功能
|
||||
|
||||
---
|
||||
|
||||
## 其他问题
|
||||
|
||||
### Q: WPBridge 是免费的吗?
|
||||
|
||||
基础功能免费,高级功能(如多站点支持)可能需要付费。
|
||||
|
||||
### Q: 如何获取技术支持?
|
||||
|
||||
- **文档**:https://wenpai.org/docs/wpbridge
|
||||
- **问题反馈**:https://github.com/ArkPress/wpbridge/issues
|
||||
- **社区支持**:https://wenpai.org/community
|
||||
|
||||
### Q: 如何参与开发?
|
||||
|
||||
WPBridge 是开源项目,欢迎贡献:
|
||||
1. Fork 仓库
|
||||
2. 创建功能分支
|
||||
3. 提交 Pull Request
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-02-05*
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# WPBridge 文档
|
||||
|
||||
> 文派云桥 - 自定义源桥接器
|
||||
|
||||
## 文档目录
|
||||
|
||||
### 用户文档
|
||||
|
||||
- [用户指南](USER-GUIDE.md) - 安装、配置和使用说明
|
||||
- [常见问题](FAQ.md) - FAQ 和故障排除
|
||||
- [API 文档](API.md) - REST API 接口文档
|
||||
|
||||
### 开发文档
|
||||
|
||||
- [CLAUDE.md](../CLAUDE.md) - 项目概述和 AI 协作指南
|
||||
- [ROADMAP.md](../ROADMAP.md) - 开发路线图
|
||||
- [ARCHITECTURE.md](../ARCHITECTURE.md) - 系统架构设计
|
||||
- [DESIGN.md](../DESIGN.md) - 技术设计文档
|
||||
|
||||
### 其他文档
|
||||
|
||||
- [RESEARCH.md](../RESEARCH.md) - 市场研究报告
|
||||
- [DISCUSSION.md](../DISCUSSION.md) - 讨论记录
|
||||
|
||||
---
|
||||
|
||||
## 快速链接
|
||||
|
||||
- **安装指南**: [USER-GUIDE.md#安装](USER-GUIDE.md#安装)
|
||||
- **更新源配置**: [USER-GUIDE.md#更新源管理](USER-GUIDE.md#更新源管理)
|
||||
- **WP-CLI 命令**: [USER-GUIDE.md#wp-cli-命令](USER-GUIDE.md#wp-cli-命令)
|
||||
- **API 认证**: [API.md#认证](API.md#认证)
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-02-05*
|
||||
|
|
@ -1,756 +0,0 @@
|
|||
# WPBridge 增值方案规划
|
||||
|
||||
> 创建日期: 2026-02-15
|
||||
> 状态: 规划中
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文档规划 WPBridge 的三个核心增值方向:
|
||||
1. 商业插件桥接(付费核心)
|
||||
2. 企业级更新管控
|
||||
3. 多站点同步管理
|
||||
|
||||
---
|
||||
|
||||
## 方案一:商业插件桥接(付费核心)
|
||||
|
||||
### 1.1 定位
|
||||
|
||||
为购买了商业插件但授权过期/无法续费的用户提供替代更新源。
|
||||
|
||||
### 1.2 架构
|
||||
|
||||
```
|
||||
用户站点 (wpbridge) 文派云桥服务端
|
||||
┌─────────────────┐ ┌─────────────────────┐
|
||||
│ CommercialDetector │ ──检测──→ │ plugin-registry │
|
||||
│ (已有) │ │ (已有 wenpai-bridge)│
|
||||
├─────────────────┤ ├─────────────────────┤
|
||||
│ LicenseProxy │ ──验证──→ │ /license/verify │
|
||||
│ (新增) │ │ (新增) │
|
||||
├─────────────────┤ ├─────────────────────┤
|
||||
│ UpdateBridge │ ──更新──→ │ /plugins/{slug}/info│
|
||||
│ (已有 JsonHandler)│ │ (已有) │
|
||||
└─────────────────┘ └─────────────────────┘
|
||||
```
|
||||
|
||||
### 1.3 核心功能
|
||||
|
||||
| 功能 | 说明 | 实现难度 | 状态 |
|
||||
|------|------|----------|------|
|
||||
| 商业插件检测 | CommercialDetector,支持 15+ 插件 | - | ✅ 已完成 |
|
||||
| 授权代理 | 拦截原厂授权请求,转发到文派验证 | 中 | 待开发 |
|
||||
| 更新桥接 | JsonHandler + wenpai-bridge | - | ✅ 已完成 |
|
||||
| 下载代理 | 从文派 CDN 下载,避免原厂限制 | 低 | 待开发 |
|
||||
|
||||
### 1.4 新增组件:LicenseProxy
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace WPBridge\Commercial;
|
||||
|
||||
class LicenseProxy {
|
||||
/**
|
||||
* 支持的授权系统
|
||||
*/
|
||||
private array $supported_vendors = [
|
||||
'edd' => [
|
||||
'name' => 'EDD Software Licensing',
|
||||
'patterns' => [
|
||||
'api.example.com/edd-sl',
|
||||
'example.com/edd-api',
|
||||
],
|
||||
'actions' => ['activate_license', 'deactivate_license', 'check_license'],
|
||||
],
|
||||
'freemius' => [
|
||||
'name' => 'Freemius',
|
||||
'patterns' => [
|
||||
'api.freemius.com',
|
||||
],
|
||||
'actions' => ['activate', 'deactivate', 'ping'],
|
||||
],
|
||||
'envato' => [
|
||||
'name' => 'Envato Market',
|
||||
'patterns' => [
|
||||
'api.envato.com',
|
||||
'envato.developer.com',
|
||||
],
|
||||
'actions' => ['verify-purchase'],
|
||||
],
|
||||
'wc_am' => [
|
||||
'name' => 'WooCommerce API Manager',
|
||||
'patterns' => [
|
||||
'wc-api/wc-am-api',
|
||||
'wc-api/am-software-api',
|
||||
],
|
||||
'actions' => ['activation', 'deactivation', 'status'],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
public function init(): void {
|
||||
add_filter('pre_http_request', [$this, 'intercept_license_check'], 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截授权验证请求
|
||||
*/
|
||||
public function intercept_license_check($preempt, array $args, string $url) {
|
||||
// 1. 检测是否是已知商业插件的授权 API
|
||||
$vendor = $this->detect_vendor($url);
|
||||
if (!$vendor) {
|
||||
return $preempt;
|
||||
}
|
||||
|
||||
// 2. 检查该插件是否在桥接列表中
|
||||
$plugin_slug = $this->extract_plugin_slug($url, $args);
|
||||
if (!$this->is_bridged_plugin($plugin_slug)) {
|
||||
return $preempt;
|
||||
}
|
||||
|
||||
// 3. 转发到文派授权代理
|
||||
return $this->proxy_to_wenpai($vendor, $plugin_slug, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测授权系统供应商
|
||||
*/
|
||||
private function detect_vendor(string $url): ?string {
|
||||
foreach ($this->supported_vendors as $vendor_key => $vendor_config) {
|
||||
foreach ($vendor_config['patterns'] as $pattern) {
|
||||
if (strpos($url, $pattern) !== false) {
|
||||
return $vendor_key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转发到文派授权代理
|
||||
*/
|
||||
private function proxy_to_wenpai(string $vendor, string $plugin_slug, array $args): array {
|
||||
$wenpai_url = 'https://updates.wenpai.net/api/v1/license/proxy';
|
||||
|
||||
$response = wp_remote_post($wenpai_url, [
|
||||
'timeout' => 15,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-WPBridge-Key' => $this->get_api_key(),
|
||||
'X-WPBridge-Site' => home_url(),
|
||||
],
|
||||
'body' => wp_json_encode([
|
||||
'vendor' => $vendor,
|
||||
'plugin_slug' => $plugin_slug,
|
||||
'action' => $this->extract_action($args),
|
||||
'site_url' => home_url(),
|
||||
]),
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// 转换响应格式以匹配原厂 API
|
||||
return $this->transform_response($vendor, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件是否在桥接列表中
|
||||
*/
|
||||
private function is_bridged_plugin(string $plugin_slug): bool {
|
||||
$bridged_plugins = get_option('wpbridge_bridged_plugins', []);
|
||||
return in_array($plugin_slug, $bridged_plugins, true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 服务端扩展 (wenpai-bridge)
|
||||
|
||||
```go
|
||||
// internal/license/proxy.go
|
||||
|
||||
package license
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ProxyRequest struct {
|
||||
Vendor string `json:"vendor"`
|
||||
PluginSlug string `json:"plugin_slug"`
|
||||
Action string `json:"action"`
|
||||
SiteURL string `json:"site_url"`
|
||||
}
|
||||
|
||||
type ProxyResponse struct {
|
||||
Success bool `json:"success"`
|
||||
License LicenseInfo `json:"license,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type LicenseInfo struct {
|
||||
Status string `json:"status"` // valid, expired, disabled
|
||||
Expires string `json:"expires"` // 2027-01-01
|
||||
LicenseLimit int `json:"license_limit"` // 站点数限制
|
||||
SiteCount int `json:"site_count"` // 已激活站点数
|
||||
Features []string `json:"features"` // updates, support, addons
|
||||
}
|
||||
|
||||
// HandleLicenseProxy 处理授权代理请求
|
||||
func (h *Handler) HandleLicenseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
var req ProxyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 验证 API Key
|
||||
apiKey := r.Header.Get("X-WPBridge-Key")
|
||||
if !h.validateAPIKey(apiKey) {
|
||||
respondError(w, "invalid api key", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 检查插件是否在桥接列表
|
||||
plugin, err := h.registry.GetPlugin(req.PluginSlug)
|
||||
if err != nil || !plugin.BridgeEnabled {
|
||||
respondError(w, "plugin not bridged", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 检查用户订阅状态
|
||||
subscription, err := h.subscriptions.GetByAPIKey(apiKey)
|
||||
if err != nil || !subscription.IsActive() {
|
||||
respondError(w, "subscription required", http.StatusPaymentRequired)
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 检查站点激活限制
|
||||
if !h.checkSiteLimit(subscription, req.SiteURL) {
|
||||
respondError(w, "site limit exceeded", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 返回授权信息
|
||||
license := LicenseInfo{
|
||||
Status: "valid",
|
||||
Expires: subscription.ExpiresAt.Format("2006-01-02"),
|
||||
LicenseLimit: subscription.SiteLimit,
|
||||
SiteCount: h.getSiteCount(subscription.ID),
|
||||
Features: []string{"updates"},
|
||||
}
|
||||
|
||||
respondJSON(w, ProxyResponse{
|
||||
Success: true,
|
||||
License: license,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 1.6 数据库设计
|
||||
|
||||
```sql
|
||||
-- 桥接插件表
|
||||
CREATE TABLE wpbridge_bridged_plugins (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(255) NOT NULL UNIQUE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
vendor VARCHAR(50) NOT NULL, -- edd, freemius, envato, wc_am
|
||||
original_api_url VARCHAR(500),
|
||||
bridge_enabled BOOLEAN DEFAULT TRUE,
|
||||
download_source VARCHAR(500), -- 文派 CDN 地址
|
||||
last_version VARCHAR(50),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_vendor (vendor),
|
||||
INDEX idx_enabled (bridge_enabled)
|
||||
);
|
||||
|
||||
-- 用户订阅表
|
||||
CREATE TABLE wpbridge_subscriptions (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
api_key VARCHAR(64) NOT NULL UNIQUE,
|
||||
plan ENUM('free', 'pro', 'agency') DEFAULT 'free',
|
||||
site_limit INT DEFAULT 1,
|
||||
plugins_limit INT DEFAULT 0, -- 0 = unlimited for agency
|
||||
status ENUM('active', 'expired', 'cancelled') DEFAULT 'active',
|
||||
expires_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_status (status)
|
||||
);
|
||||
|
||||
-- 站点激活记录表
|
||||
CREATE TABLE wpbridge_site_activations (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
subscription_id BIGINT UNSIGNED NOT NULL,
|
||||
site_url VARCHAR(500) NOT NULL,
|
||||
site_hash VARCHAR(64) NOT NULL, -- SHA256(site_url)
|
||||
activated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_seen DATETIME,
|
||||
UNIQUE KEY uk_sub_site (subscription_id, site_hash),
|
||||
INDEX idx_subscription (subscription_id)
|
||||
);
|
||||
```
|
||||
|
||||
### 1.7 商业模式
|
||||
|
||||
| 层级 | 价格 | 功能 |
|
||||
|------|------|------|
|
||||
| Free | 免费 | 开源插件更新、商业插件检测 |
|
||||
| Pro | ¥199/年 | 商业插件更新桥接(5个插件,3个站点) |
|
||||
| Agency | ¥999/年 | 无限插件 + 无限站点 + 授权代理 + 优先支持 |
|
||||
|
||||
### 1.8 支持的商业插件(初期)
|
||||
|
||||
| 插件 | 授权系统 | 优先级 |
|
||||
|------|----------|--------|
|
||||
| Elementor Pro | EDD | 高 |
|
||||
| Yoast SEO Premium | EDD | 高 |
|
||||
| ACF Pro | EDD | 高 |
|
||||
| Gravity Forms | EDD | 高 |
|
||||
| WP Rocket | Custom | 中 |
|
||||
| Rank Math Pro | Freemius | 中 |
|
||||
| WPForms Pro | EDD | 中 |
|
||||
|
||||
### 1.9 风险与合规
|
||||
|
||||
#### GPL 合规
|
||||
- 只桥接 GPL 授权的插件更新
|
||||
- 不破解非 GPL 插件(如 Envato 独占插件)
|
||||
- 明确声明"替代更新源"而非"破解授权"
|
||||
|
||||
#### 法律风险
|
||||
- 可能被原厂封禁 API
|
||||
- 需要备用下载源(文派 CDN)
|
||||
- 用户协议明确免责条款
|
||||
|
||||
#### 技术风险
|
||||
- 原厂 API 变更需要及时适配
|
||||
- 需要持续维护插件兼容性
|
||||
- 下载包需要安全扫描
|
||||
|
||||
---
|
||||
|
||||
## 方案二:企业级更新管控
|
||||
|
||||
### 2.1 定位
|
||||
|
||||
为企业/代理商提供 WordPress 更新的集中管控能力。
|
||||
|
||||
### 2.2 核心功能
|
||||
|
||||
| 功能 | 说明 | 优先级 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| 版本锁定 | 锁定到指定版本,阻止自动更新 | 高 | 已有基础 |
|
||||
| 更新审批 | 更新前需管理员审批 | 高 | 待开发 |
|
||||
| 回滚机制 | 更新失败自动回滚 | 高 | 待开发 |
|
||||
| 更新日志 | 聚合显示所有插件 changelog | 中 | 待开发 |
|
||||
| 安全扫描 | 更新前检查 VirusTotal | 中 | 待开发 |
|
||||
|
||||
### 2.3 新增组件:UpdateApproval
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace WPBridge\Enterprise;
|
||||
|
||||
class UpdateApproval {
|
||||
const STATUS_PENDING = 'pending';
|
||||
const STATUS_APPROVED = 'approved';
|
||||
const STATUS_REJECTED = 'rejected';
|
||||
const STATUS_AUTO = 'auto_approved';
|
||||
|
||||
/**
|
||||
* 审批规则
|
||||
*/
|
||||
private array $rules = [];
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public function init(): void {
|
||||
add_filter('pre_set_site_transient_update_plugins', [$this, 'filter_updates'], 100);
|
||||
add_filter('pre_set_site_transient_update_themes', [$this, 'filter_theme_updates'], 100);
|
||||
add_action('admin_menu', [$this, 'add_approval_menu']);
|
||||
add_action('wp_ajax_wpbridge_approve_update', [$this, 'ajax_approve']);
|
||||
add_action('wp_ajax_wpbridge_reject_update', [$this, 'ajax_reject']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤更新,创建审批请求
|
||||
*/
|
||||
public function filter_updates($transient) {
|
||||
if (empty($transient->response)) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
foreach ($transient->response as $file => $update) {
|
||||
$slug = dirname($file);
|
||||
|
||||
// 检查是否需要审批
|
||||
if ($this->requires_approval($slug, $update)) {
|
||||
// 检查是否已审批
|
||||
if (!$this->is_approved($slug, $update->new_version)) {
|
||||
// 创建审批请求
|
||||
$this->create_approval_request($file, $update);
|
||||
// 从更新列表移除
|
||||
unset($transient->response[$file]);
|
||||
// 添加到待审批列表
|
||||
$transient->no_update[$file] = $update;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $transient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要审批
|
||||
*/
|
||||
private function requires_approval(string $slug, object $update): bool {
|
||||
// 规则 1: 主版本更新需要审批
|
||||
if ($this->is_major_update($update)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 规则 2: 指定插件需要审批
|
||||
$require_approval_list = get_option('wpbridge_require_approval', []);
|
||||
if (in_array($slug, $require_approval_list, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 规则 3: 全局审批模式
|
||||
$global_mode = get_option('wpbridge_approval_mode', 'none');
|
||||
if ($global_mode === 'all') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建审批请求
|
||||
*/
|
||||
private function create_approval_request(string $file, object $update): int {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'wpbridge_approvals';
|
||||
|
||||
// 检查是否已存在
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM $table WHERE plugin_file = %s AND new_version = %s AND status = %s",
|
||||
$file,
|
||||
$update->new_version,
|
||||
self::STATUS_PENDING
|
||||
));
|
||||
|
||||
if ($existing) {
|
||||
return (int) $existing;
|
||||
}
|
||||
|
||||
// 获取 changelog
|
||||
$changelog = $this->fetch_changelog($update);
|
||||
|
||||
$wpdb->insert($table, [
|
||||
'plugin_file' => $file,
|
||||
'plugin_name' => $this->get_plugin_name($file),
|
||||
'current_version' => $this->get_current_version($file),
|
||||
'new_version' => $update->new_version,
|
||||
'changelog' => $changelog,
|
||||
'status' => self::STATUS_PENDING,
|
||||
'created_at' => current_time('mysql'),
|
||||
]);
|
||||
|
||||
// 发送通知
|
||||
$this->notify_admins($file, $update);
|
||||
|
||||
return $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批更新
|
||||
*/
|
||||
public function approve(int $approval_id, int $user_id): bool {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'wpbridge_approvals';
|
||||
|
||||
return (bool) $wpdb->update(
|
||||
$table,
|
||||
[
|
||||
'status' => self::STATUS_APPROVED,
|
||||
'approved_by' => $user_id,
|
||||
'approved_at' => current_time('mysql'),
|
||||
],
|
||||
['id' => $approval_id]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝更新
|
||||
*/
|
||||
public function reject(int $approval_id, int $user_id, string $reason = ''): bool {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'wpbridge_approvals';
|
||||
|
||||
return (bool) $wpdb->update(
|
||||
$table,
|
||||
[
|
||||
'status' => self::STATUS_REJECTED,
|
||||
'approved_by' => $user_id,
|
||||
'approved_at' => current_time('mysql'),
|
||||
'reject_reason' => $reason,
|
||||
],
|
||||
['id' => $approval_id]
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 新增组件:BackupManager(扩展)
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace WPBridge\Enterprise;
|
||||
|
||||
class BackupManager {
|
||||
const BACKUP_DIR = 'wpbridge-backups';
|
||||
const MAX_BACKUPS = 3;
|
||||
|
||||
/**
|
||||
* 更新前自动备份
|
||||
*/
|
||||
public function pre_update_backup(string $plugin_file): ?string {
|
||||
$plugin_dir = WP_PLUGIN_DIR . '/' . dirname($plugin_file);
|
||||
|
||||
if (!is_dir($plugin_dir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$backup_path = $this->create_backup($plugin_file);
|
||||
|
||||
if ($backup_path) {
|
||||
$this->store_backup_meta($plugin_file, $backup_path);
|
||||
$this->cleanup_old_backups($plugin_file);
|
||||
}
|
||||
|
||||
return $backup_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建备份
|
||||
*/
|
||||
private function create_backup(string $plugin_file): ?string {
|
||||
$plugin_slug = dirname($plugin_file);
|
||||
$plugin_dir = WP_PLUGIN_DIR . '/' . $plugin_slug;
|
||||
$version = $this->get_plugin_version($plugin_file);
|
||||
|
||||
$backup_dir = WP_CONTENT_DIR . '/' . self::BACKUP_DIR;
|
||||
if (!is_dir($backup_dir)) {
|
||||
wp_mkdir_p($backup_dir);
|
||||
// 添加 .htaccess 保护
|
||||
file_put_contents($backup_dir . '/.htaccess', 'deny from all');
|
||||
}
|
||||
|
||||
$backup_filename = sprintf(
|
||||
'%s-%s-%s.zip',
|
||||
$plugin_slug,
|
||||
$version,
|
||||
date('Ymd-His')
|
||||
);
|
||||
$backup_path = $backup_dir . '/' . $backup_filename;
|
||||
|
||||
// 创建 ZIP
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($backup_path, \ZipArchive::CREATE) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->add_dir_to_zip($zip, $plugin_dir, $plugin_slug);
|
||||
$zip->close();
|
||||
|
||||
return $backup_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键回滚
|
||||
*/
|
||||
public function rollback(string $plugin_file, ?string $version = null): bool {
|
||||
$backup = $this->get_backup($plugin_file, $version);
|
||||
|
||||
if (!$backup || !file_exists($backup['path'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 停用插件
|
||||
deactivate_plugins($plugin_file);
|
||||
|
||||
// 删除当前版本
|
||||
$plugin_dir = WP_PLUGIN_DIR . '/' . dirname($plugin_file);
|
||||
$this->delete_directory($plugin_dir);
|
||||
|
||||
// 解压备份
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($backup['path']) !== true) {
|
||||
return false;
|
||||
}
|
||||
$zip->extractTo(WP_PLUGIN_DIR);
|
||||
$zip->close();
|
||||
|
||||
// 重新激活插件
|
||||
activate_plugin($plugin_file);
|
||||
|
||||
// 记录回滚
|
||||
$this->log_rollback($plugin_file, $backup['version']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份列表
|
||||
*/
|
||||
public function get_backups(string $plugin_file): array {
|
||||
$backups = get_option('wpbridge_backups', []);
|
||||
$plugin_slug = dirname($plugin_file);
|
||||
|
||||
return $backups[$plugin_slug] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧备份
|
||||
*/
|
||||
private function cleanup_old_backups(string $plugin_file): void {
|
||||
$backups = $this->get_backups($plugin_file);
|
||||
|
||||
if (count($backups) <= self::MAX_BACKUPS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 按时间排序,删除最旧的
|
||||
usort($backups, fn($a, $b) => $b['created_at'] <=> $a['created_at']);
|
||||
|
||||
$to_delete = array_slice($backups, self::MAX_BACKUPS);
|
||||
foreach ($to_delete as $backup) {
|
||||
if (file_exists($backup['path'])) {
|
||||
unlink($backup['path']);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新记录
|
||||
$plugin_slug = dirname($plugin_file);
|
||||
$all_backups = get_option('wpbridge_backups', []);
|
||||
$all_backups[$plugin_slug] = array_slice($backups, 0, self::MAX_BACKUPS);
|
||||
update_option('wpbridge_backups', $all_backups);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 数据库表
|
||||
|
||||
```sql
|
||||
-- 审批请求表
|
||||
CREATE TABLE wp_wpbridge_approvals (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
plugin_file VARCHAR(255) NOT NULL,
|
||||
plugin_name VARCHAR(255),
|
||||
current_version VARCHAR(50),
|
||||
new_version VARCHAR(50) NOT NULL,
|
||||
changelog TEXT,
|
||||
status ENUM('pending', 'approved', 'rejected', 'auto_approved') DEFAULT 'pending',
|
||||
approved_by BIGINT UNSIGNED,
|
||||
approved_at DATETIME,
|
||||
reject_reason TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_plugin (plugin_file),
|
||||
INDEX idx_created (created_at)
|
||||
);
|
||||
```
|
||||
|
||||
### 2.6 商业模式
|
||||
|
||||
| 层级 | 价格 | 功能 |
|
||||
|------|------|------|
|
||||
| Free | 免费 | 版本锁定(3个插件) |
|
||||
| Pro | ¥299/年 | 无限锁定 + 回滚 + 更新日志 |
|
||||
| Enterprise | ¥1999/年 | 审批流程 + 安全扫描 + API + 多用户 |
|
||||
|
||||
---
|
||||
|
||||
## 方案三:多站点同步管理
|
||||
|
||||
### 3.1 定位
|
||||
|
||||
为管理多个 WordPress 站点的代理商/企业提供统一配置管理。
|
||||
|
||||
### 3.2 架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ WPBridge Hub (中心) │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ 配置管理 │ │ 站点监控 │ │ 批量操作 │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ 站点 A │ │ 站点 B │ │ 站点 C │
|
||||
│ wpbridge │ │ wpbridge │ │ wpbridge │
|
||||
│ (Agent) │ │ (Agent) │ │ (Agent) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### 3.3 核心功能
|
||||
|
||||
| 功能 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 配置同步 | 中心配置自动同步到所有站点 | 高 |
|
||||
| 站点监控 | 实时监控所有站点更新状态 | 高 |
|
||||
| 批量更新 | 一键更新所有站点的指定插件 | 中 |
|
||||
| 分组管理 | 按客户/项目分组管理站点 | 中 |
|
||||
| 报告生成 | 生成更新状态报告 | 低 |
|
||||
|
||||
### 3.4 实现方式
|
||||
|
||||
推荐 SaaS 中心化方案,在 wenpai.net 上提供 Hub 服务。
|
||||
|
||||
### 3.5 商业模式
|
||||
|
||||
| 层级 | 价格 | 功能 |
|
||||
|------|------|------|
|
||||
| Free | 免费 | 单站点使用 |
|
||||
| Pro | ¥499/年 | 最多 10 个站点同步 |
|
||||
| Agency | ¥1999/年 | 最多 100 个站点 + 白标 |
|
||||
| Enterprise | ¥9999/年 | 无限站点 + 自托管 Hub + API |
|
||||
|
||||
---
|
||||
|
||||
## 方案对比
|
||||
|
||||
| 维度 | 商业插件桥接 | 企业级管控 | 多站点同步 |
|
||||
|------|-------------|-----------|-----------|
|
||||
| 开发难度 | 中 | 中 | 高 |
|
||||
| 市场需求 | 高(刚需) | 中 | 中 |
|
||||
| 付费意愿 | 高 | 中 | 高(代理商) |
|
||||
| 法律风险 | 中 | 低 | 低 |
|
||||
| 竞品情况 | 少 | 多 | 少 |
|
||||
| 与现有代码复用 | 高 | 高 | 中 |
|
||||
|
||||
---
|
||||
|
||||
## 建议优先级
|
||||
|
||||
1. **先完成 v0.9.0 路线图**(版本锁定 + 回滚)- 企业级管控的基础
|
||||
2. **商业插件桥接** - 差异化竞争点,与 wenpai-bridge 协同
|
||||
3. **多站点同步** - 作为 Pro/Agency 版本的高级功能
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-02-15*
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
# WPBridge 用户指南
|
||||
|
||||
> 自定义源桥接器 - 让用户完全控制 WordPress 的外部连接
|
||||
|
||||
## 目录
|
||||
|
||||
- [简介](#简介)
|
||||
- [安装](#安装)
|
||||
- [快速开始](#快速开始)
|
||||
- [更新源管理](#更新源管理)
|
||||
- [商业插件检测](#商业插件检测)
|
||||
- [配置导入导出](#配置导入导出)
|
||||
- [WP-CLI 命令](#wp-cli-命令)
|
||||
- [Bridge API](#bridge-api)
|
||||
- [常见问题](#常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 简介
|
||||
|
||||
WPBridge(文派云桥)是一个 WordPress 插件,允许您配置自定义的插件和主题更新源。
|
||||
|
||||
### 主要功能
|
||||
|
||||
- **自定义更新源**:支持 JSON API、GitHub、GitLab、Gitee 等多种更新源
|
||||
- **商业插件管理**:自动检测商业插件,支持自定义更新源
|
||||
- **源分组**:批量管理多个更新源
|
||||
- **Bridge API**:提供 REST API 供外部调用
|
||||
- **WP-CLI 支持**:命令行管理更新源
|
||||
|
||||
### 适用场景
|
||||
|
||||
- 企业内网部署,需要私有更新服务器
|
||||
- 商业插件用户,需要统一管理更新源
|
||||
- 开发者测试环境
|
||||
- 需要 AI 服务桥接的用户
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
### 要求
|
||||
|
||||
- WordPress 5.9+
|
||||
- PHP 7.4+
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 下载插件 ZIP 文件
|
||||
2. 在 WordPress 后台进入「插件 > 安装插件 > 上传插件」
|
||||
3. 上传并激活插件
|
||||
4. 进入「设置 > WPBridge」配置插件
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 访问设置页面
|
||||
|
||||
激活插件后,在 WordPress 后台菜单找到「设置 > WPBridge」。
|
||||
|
||||
### 2. 查看概览
|
||||
|
||||
概览页面显示:
|
||||
- 已配置的更新源数量
|
||||
- 源健康状态
|
||||
- 最近的更新检查
|
||||
|
||||
### 3. 添加更新源
|
||||
|
||||
1. 点击「更新源」标签
|
||||
2. 点击「添加更新源」按钮
|
||||
3. 填写更新源信息:
|
||||
- **名称**:更新源的显示名称
|
||||
- **类型**:JSON API / GitHub / GitLab / Gitee 等
|
||||
- **URL**:更新源的 API 地址
|
||||
- **项目类型**:插件或主题
|
||||
4. 点击「保存」
|
||||
|
||||
---
|
||||
|
||||
## 更新源管理
|
||||
|
||||
### 支持的更新源类型
|
||||
|
||||
| 类型 | 说明 | URL 格式 |
|
||||
|------|------|----------|
|
||||
| JSON API | 标准 JSON 格式 | `https://example.com/updates.json` |
|
||||
| GitHub | GitHub Releases | `https://github.com/owner/repo` |
|
||||
| GitLab | GitLab Releases | `https://gitlab.com/owner/repo` |
|
||||
| Gitee | Gitee Releases | `https://gitee.com/owner/repo` |
|
||||
| ArkPress | 文派自托管方案 | `https://api.example.com/v1` |
|
||||
| AspireCloud | AspireCloud 服务 | `https://api.aspirecloud.com` |
|
||||
| PUC | Plugin Update Checker | `https://example.com/plugin-info.json` |
|
||||
|
||||
### 更新源优先级
|
||||
|
||||
当多个更新源提供同一插件的更新时,WPBridge 会:
|
||||
1. 按优先级排序(数字越小优先级越高)
|
||||
2. 选择版本号最高的更新
|
||||
|
||||
### 认证配置
|
||||
|
||||
对于需要认证的更新源:
|
||||
1. 在更新源设置中填写 API Token
|
||||
2. 支持 API Key、Basic Auth、自定义 HTTP 头
|
||||
|
||||
---
|
||||
|
||||
## 商业插件检测
|
||||
|
||||
WPBridge 可以自动检测已安装的商业插件。
|
||||
|
||||
### 检测方式
|
||||
|
||||
1. **远程配置**:从云端获取已知商业插件列表
|
||||
2. **WordPress.org 检查**:不在官方目录的插件标记为第三方
|
||||
3. **手动标记**:用户可手动设置插件类型
|
||||
|
||||
### 插件类型
|
||||
|
||||
- **免费**:WordPress.org 官方目录中的插件
|
||||
- **商业**:已知的商业插件
|
||||
- **第三方**:不在官方目录的其他插件
|
||||
|
||||
### 刷新检测
|
||||
|
||||
点击「刷新检测」按钮可重新检测所有插件类型。
|
||||
|
||||
---
|
||||
|
||||
## 配置导入导出
|
||||
|
||||
### 导出配置
|
||||
|
||||
1. 进入「设置」标签
|
||||
2. 在「配置导入导出」区域点击「导出」
|
||||
3. 可选择是否包含敏感信息(API Key 等)
|
||||
4. 下载 JSON 配置文件
|
||||
|
||||
### 导入配置
|
||||
|
||||
1. 点击「导入」按钮
|
||||
2. 选择之前导出的 JSON 文件
|
||||
3. 选择导入模式:
|
||||
- **合并**:与现有配置合并
|
||||
- **覆盖**:完全替换现有配置
|
||||
4. 确认导入
|
||||
|
||||
---
|
||||
|
||||
## WP-CLI 命令
|
||||
|
||||
WPBridge 提供完整的 WP-CLI 支持。
|
||||
|
||||
### 更新源管理
|
||||
|
||||
```bash
|
||||
# 列出所有更新源
|
||||
wp bridge source list
|
||||
|
||||
# 添加更新源
|
||||
wp bridge source add https://example.com/updates.json --name="My Source"
|
||||
|
||||
# 删除更新源
|
||||
wp bridge source remove <source_id>
|
||||
|
||||
# 启用/禁用更新源
|
||||
wp bridge source enable <source_id>
|
||||
wp bridge source disable <source_id>
|
||||
```
|
||||
|
||||
### 缓存管理
|
||||
|
||||
```bash
|
||||
# 清除缓存
|
||||
wp bridge cache clear
|
||||
|
||||
# 查看缓存状态
|
||||
wp bridge cache status
|
||||
```
|
||||
|
||||
### 诊断
|
||||
|
||||
```bash
|
||||
# 检查所有更新源
|
||||
wp bridge check
|
||||
|
||||
# 运行诊断
|
||||
wp bridge diagnose
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
```bash
|
||||
# 导出配置
|
||||
wp bridge config export /path/to/config.json
|
||||
|
||||
# 导入配置
|
||||
wp bridge config import /path/to/config.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bridge API
|
||||
|
||||
WPBridge 提供 REST API 供外部调用。
|
||||
|
||||
### 启用 API
|
||||
|
||||
1. 进入「API」标签
|
||||
2. 点击「生成 API Key」
|
||||
3. 保存生成的 Key(只显示一次)
|
||||
|
||||
### API 端点
|
||||
|
||||
```
|
||||
GET /wp-json/bridge/v1/status
|
||||
```
|
||||
|
||||
返回插件状态信息。
|
||||
|
||||
### 认证
|
||||
|
||||
在请求头中添加:
|
||||
```
|
||||
X-WPBridge-Key: your_api_key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 更新源不工作怎么办?
|
||||
|
||||
1. 检查更新源 URL 是否正确
|
||||
2. 在「诊断」页面测试源连通性
|
||||
3. 检查是否需要认证信息
|
||||
4. 查看调试日志(需启用调试模式)
|
||||
|
||||
### Q: 如何处理商业插件更新?
|
||||
|
||||
1. WPBridge 会自动检测商业插件
|
||||
2. 您可以为商业插件配置自定义更新源
|
||||
3. 或者手动标记插件类型
|
||||
|
||||
### Q: 配置丢失怎么恢复?
|
||||
|
||||
1. 如果有备份,使用「导入配置」功能恢复
|
||||
2. 如果没有备份,需要重新配置
|
||||
|
||||
### Q: 如何与文派叶子配合使用?
|
||||
|
||||
WPBridge 会自动检测文派叶子(WPCY)的存在:
|
||||
- 官方源更新走 WPCY 加速
|
||||
- 自定义源走 WPBridge 配置
|
||||
|
||||
---
|
||||
|
||||
## 获取帮助
|
||||
|
||||
- **文档**:https://wenpai.org/docs/wpbridge
|
||||
- **问题反馈**:https://github.com/ArkPress/wpbridge/issues
|
||||
- **社区支持**:https://wenpai.org/community
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-02-05*
|
||||
|
|
@ -13,7 +13,7 @@ use WPBridge\Security\Validator;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,342 +22,333 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class AIGateway {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 已注册的适配器
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $adapters = array();
|
||||
/**
|
||||
* 已注册的适配器
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $adapters = [];
|
||||
|
||||
/**
|
||||
* 白名单域名
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $whitelist = array();
|
||||
/**
|
||||
* 白名单域名
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $whitelist = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->whitelist = $this->get_whitelist();
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->whitelist = $this->get_whitelist();
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 只有启用 AI 桥接时才注册钩子
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 只有启用 AI 桥接时才注册钩子
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter( 'pre_http_request', array( $this, 'intercept_request' ), 10, 3 );
|
||||
}
|
||||
add_filter( 'pre_http_request', [ $this, 'intercept_request' ], 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用 AI 桥接
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool {
|
||||
$ai_settings = $this->settings->get( 'ai_bridge', array() );
|
||||
return ! empty( $ai_settings['enabled'] );
|
||||
}
|
||||
/**
|
||||
* 是否启用 AI 桥接
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool {
|
||||
$ai_settings = $this->settings->get( 'ai_bridge', [] );
|
||||
return ! empty( $ai_settings['enabled'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取桥接模式
|
||||
*
|
||||
* @return string disabled|passthrough|wpmind
|
||||
*/
|
||||
public function get_mode(): string {
|
||||
$ai_settings = $this->settings->get( 'ai_bridge', array() );
|
||||
return $ai_settings['mode'] ?? 'disabled';
|
||||
}
|
||||
/**
|
||||
* 获取桥接模式
|
||||
*
|
||||
* @return string disabled|passthrough|wpmind
|
||||
*/
|
||||
public function get_mode(): string {
|
||||
$ai_settings = $this->settings->get( 'ai_bridge', [] );
|
||||
return $ai_settings['mode'] ?? 'disabled';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取白名单
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_whitelist(): array {
|
||||
$ai_settings = $this->settings->get( 'ai_bridge', array() );
|
||||
$whitelist = $ai_settings['whitelist'] ?? array();
|
||||
/**
|
||||
* 获取白名单
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_whitelist(): array {
|
||||
$ai_settings = $this->settings->get( 'ai_bridge', [] );
|
||||
$whitelist = $ai_settings['whitelist'] ?? [];
|
||||
|
||||
// 默认白名单
|
||||
$default_whitelist = array(
|
||||
'api.openai.com',
|
||||
'api.anthropic.com',
|
||||
'generativelanguage.googleapis.com',
|
||||
);
|
||||
// 默认白名单
|
||||
$default_whitelist = [
|
||||
'api.openai.com',
|
||||
'api.anthropic.com',
|
||||
'generativelanguage.googleapis.com',
|
||||
];
|
||||
|
||||
return array_unique( array_merge( $default_whitelist, $whitelist ) );
|
||||
}
|
||||
return array_unique( array_merge( $default_whitelist, $whitelist ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截 HTTP 请求
|
||||
*
|
||||
* @param false|array|\WP_Error $preempt 预处理结果
|
||||
* @param array $args 请求参数
|
||||
* @param string $url 请求 URL
|
||||
* @return false|array|\WP_Error
|
||||
*/
|
||||
public function intercept_request( $preempt, array $args, string $url ) {
|
||||
// 如果已经被其他过滤器处理,跳过
|
||||
if ( false !== $preempt ) {
|
||||
return $preempt;
|
||||
}
|
||||
/**
|
||||
* 拦截 HTTP 请求
|
||||
*
|
||||
* @param false|array|\WP_Error $preempt 预处理结果
|
||||
* @param array $args 请求参数
|
||||
* @param string $url 请求 URL
|
||||
* @return false|array|\WP_Error
|
||||
*/
|
||||
public function intercept_request( $preempt, array $args, string $url ) {
|
||||
// 如果已经被其他过滤器处理,跳过
|
||||
if ( false !== $preempt ) {
|
||||
return $preempt;
|
||||
}
|
||||
|
||||
// 检查是否是 AI API 请求
|
||||
if ( ! $this->is_ai_request( $url ) ) {
|
||||
return false;
|
||||
}
|
||||
// 检查是否是 AI API 请求
|
||||
if ( ! $this->is_ai_request( $url ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::debug( 'AI 请求拦截', array( 'url' => $url ) );
|
||||
Logger::debug( 'AI 请求拦截', [ 'url' => $url ] );
|
||||
|
||||
// 根据模式处理
|
||||
$mode = $this->get_mode();
|
||||
// 根据模式处理
|
||||
$mode = $this->get_mode();
|
||||
|
||||
switch ( $mode ) {
|
||||
case 'passthrough':
|
||||
return $this->handle_passthrough( $url, $args );
|
||||
switch ( $mode ) {
|
||||
case 'passthrough':
|
||||
return $this->handle_passthrough( $url, $args );
|
||||
|
||||
case 'wpmind':
|
||||
return $this->handle_wpmind( $url, $args );
|
||||
case 'wpmind':
|
||||
return $this->handle_wpmind( $url, $args );
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是 AI API 请求
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return bool
|
||||
*/
|
||||
private function is_ai_request( string $url ): bool {
|
||||
$host = wp_parse_url( $url, PHP_URL_HOST );
|
||||
/**
|
||||
* 检查是否是 AI API 请求
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return bool
|
||||
*/
|
||||
private function is_ai_request( string $url ): bool {
|
||||
$host = wp_parse_url( $url, PHP_URL_HOST );
|
||||
|
||||
if ( empty( $host ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( empty( $host ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = strtolower( $host );
|
||||
$host = strtolower( $host );
|
||||
|
||||
foreach ( $this->whitelist as $allowed ) {
|
||||
if ( strtolower( $allowed ) === $host ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
foreach ( $this->whitelist as $allowed ) {
|
||||
if ( strtolower( $allowed ) === $host ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 透传模式处理
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 请求参数
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
private function handle_passthrough( string $url, array $args ) {
|
||||
$ai_settings = $this->settings->get( 'ai_bridge', array() );
|
||||
$custom_endpoint = $ai_settings['custom_endpoint'] ?? '';
|
||||
/**
|
||||
* 透传模式处理
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 请求参数
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
private function handle_passthrough( string $url, array $args ) {
|
||||
$ai_settings = $this->settings->get( 'ai_bridge', [] );
|
||||
$custom_endpoint = $ai_settings['custom_endpoint'] ?? '';
|
||||
|
||||
if ( empty( $custom_endpoint ) ) {
|
||||
Logger::warning( '透传模式未配置自定义端点' );
|
||||
return false;
|
||||
}
|
||||
if ( empty( $custom_endpoint ) ) {
|
||||
Logger::warning( '透传模式未配置自定义端点' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// SSRF 防护:验证端点安全性
|
||||
if ( ! Validator::is_valid_url( $custom_endpoint ) ) {
|
||||
Logger::error( '自定义端点不安全', array( 'endpoint' => $custom_endpoint ) );
|
||||
return false;
|
||||
}
|
||||
// SSRF 防护:验证端点安全性
|
||||
if ( ! Validator::is_valid_url( $custom_endpoint ) ) {
|
||||
Logger::error( '自定义端点不安全', [ 'endpoint' => $custom_endpoint ] );
|
||||
return false;
|
||||
}
|
||||
|
||||
// 替换 URL
|
||||
$new_url = $this->replace_endpoint( $url, $custom_endpoint );
|
||||
// 替换 URL
|
||||
$new_url = $this->replace_endpoint( $url, $custom_endpoint );
|
||||
|
||||
Logger::debug(
|
||||
'AI 请求透传',
|
||||
array(
|
||||
'original' => $url,
|
||||
'new' => $new_url,
|
||||
)
|
||||
);
|
||||
Logger::debug( 'AI 请求透传', [
|
||||
'original' => $url,
|
||||
'new' => $new_url,
|
||||
] );
|
||||
|
||||
// 发送请求
|
||||
return $this->forward_request( $new_url, $args );
|
||||
}
|
||||
// 发送请求
|
||||
return $this->forward_request( $new_url, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* WPMind 模式处理
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 请求参数
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
private function handle_wpmind( string $url, array $args ) {
|
||||
// 检查 WPMind 是否可用
|
||||
if ( ! $this->is_wpmind_available() ) {
|
||||
Logger::warning( 'WPMind 不可用,回退到透传模式' );
|
||||
return $this->handle_passthrough( $url, $args );
|
||||
}
|
||||
/**
|
||||
* WPMind 模式处理
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 请求参数
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
private function handle_wpmind( string $url, array $args ) {
|
||||
// 检查 WPMind 是否可用
|
||||
if ( ! $this->is_wpmind_available() ) {
|
||||
Logger::warning( 'WPMind 不可用,回退到透传模式' );
|
||||
return $this->handle_passthrough( $url, $args );
|
||||
}
|
||||
|
||||
// 使用 WPMind API
|
||||
$wpmind_endpoint = apply_filters( 'wpmind_api_endpoint', 'https://api.wpmind.cn/v1' );
|
||||
// 使用 WPMind API
|
||||
$wpmind_endpoint = apply_filters( 'wpmind_api_endpoint', 'https://api.wpmind.cn/v1' );
|
||||
|
||||
// 转换请求格式
|
||||
$converted_args = $this->convert_to_wpmind_format( $url, $args );
|
||||
// 转换请求格式
|
||||
$converted_args = $this->convert_to_wpmind_format( $url, $args );
|
||||
|
||||
Logger::debug(
|
||||
'AI 请求转发到 WPMind',
|
||||
array(
|
||||
'original' => $url,
|
||||
'endpoint' => $wpmind_endpoint,
|
||||
)
|
||||
);
|
||||
Logger::debug( 'AI 请求转发到 WPMind', [
|
||||
'original' => $url,
|
||||
'endpoint' => $wpmind_endpoint,
|
||||
] );
|
||||
|
||||
return $this->forward_request( $wpmind_endpoint, $converted_args );
|
||||
}
|
||||
return $this->forward_request( $wpmind_endpoint, $converted_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 WPMind 是否可用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_wpmind_available(): bool {
|
||||
return class_exists( 'WPMind\\Core\\Plugin' ) ||
|
||||
function_exists( 'wpmind_get_api_key' );
|
||||
}
|
||||
/**
|
||||
* 检查 WPMind 是否可用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_wpmind_available(): bool {
|
||||
return class_exists( 'WPMind\\Core\\Plugin' ) ||
|
||||
function_exists( 'wpmind_get_api_key' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换端点
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param string $endpoint 新端点
|
||||
* @return string
|
||||
*/
|
||||
private function replace_endpoint( string $url, string $endpoint ): string {
|
||||
$parsed = wp_parse_url( $url );
|
||||
$path = $parsed['path'] ?? '';
|
||||
$query = isset( $parsed['query'] ) ? '?' . $parsed['query'] : '';
|
||||
/**
|
||||
* 替换端点
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param string $endpoint 新端点
|
||||
* @return string
|
||||
*/
|
||||
private function replace_endpoint( string $url, string $endpoint ): string {
|
||||
$parsed = wp_parse_url( $url );
|
||||
$path = $parsed['path'] ?? '';
|
||||
$query = isset( $parsed['query'] ) ? '?' . $parsed['query'] : '';
|
||||
|
||||
// 移除端点末尾的斜杠
|
||||
$endpoint = rtrim( $endpoint, '/' );
|
||||
// 移除端点末尾的斜杠
|
||||
$endpoint = rtrim( $endpoint, '/' );
|
||||
|
||||
return $endpoint . $path . $query;
|
||||
}
|
||||
return $endpoint . $path . $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 WPMind 格式
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 请求参数
|
||||
* @return array
|
||||
*/
|
||||
private function convert_to_wpmind_format( string $url, array $args ): array {
|
||||
// 获取 WPMind API Key
|
||||
$api_key = '';
|
||||
if ( function_exists( 'wpmind_get_api_key' ) ) {
|
||||
$api_key = wpmind_get_api_key();
|
||||
}
|
||||
/**
|
||||
* 转换为 WPMind 格式
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 请求参数
|
||||
* @return array
|
||||
*/
|
||||
private function convert_to_wpmind_format( string $url, array $args ): array {
|
||||
// 获取 WPMind API Key
|
||||
$api_key = '';
|
||||
if ( function_exists( 'wpmind_get_api_key' ) ) {
|
||||
$api_key = wpmind_get_api_key();
|
||||
}
|
||||
|
||||
// 更新认证头
|
||||
if ( ! empty( $api_key ) ) {
|
||||
$args['headers']['Authorization'] = 'Bearer ' . $api_key;
|
||||
}
|
||||
// 更新认证头
|
||||
if ( ! empty( $api_key ) ) {
|
||||
$args['headers']['Authorization'] = 'Bearer ' . $api_key;
|
||||
}
|
||||
|
||||
// 添加来源标识
|
||||
$args['headers']['X-WPBridge-Source'] = 'wpbridge';
|
||||
// 添加来源标识
|
||||
$args['headers']['X-WPBridge-Source'] = 'wpbridge';
|
||||
|
||||
return $args;
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转发请求
|
||||
*
|
||||
* @param string $url URL
|
||||
* @param array $args 请求参数
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
private function forward_request( string $url, array $args ) {
|
||||
// 移除 pre_http_request 过滤器避免递归
|
||||
remove_filter( 'pre_http_request', array( $this, 'intercept_request' ), 10 );
|
||||
/**
|
||||
* 转发请求
|
||||
*
|
||||
* @param string $url URL
|
||||
* @param array $args 请求参数
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
private function forward_request( string $url, array $args ) {
|
||||
// 移除 pre_http_request 过滤器避免递归
|
||||
remove_filter( 'pre_http_request', [ $this, 'intercept_request' ], 10 );
|
||||
|
||||
$response = wp_remote_request( $url, $args );
|
||||
$response = wp_remote_request( $url, $args );
|
||||
|
||||
// 重新添加过滤器
|
||||
add_filter( 'pre_http_request', array( $this, 'intercept_request' ), 10, 3 );
|
||||
// 重新添加过滤器
|
||||
add_filter( 'pre_http_request', [ $this, 'intercept_request' ], 10, 3 );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error(
|
||||
'AI 请求转发失败',
|
||||
array(
|
||||
'url' => $url,
|
||||
'error' => $response->get_error_message(),
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error( 'AI 请求转发失败', [
|
||||
'url' => $url,
|
||||
'error' => $response->get_error_message(),
|
||||
] );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册适配器
|
||||
*
|
||||
* @param string $name 适配器名称
|
||||
* @param Adapters\AdapterInterface $adapter 适配器实例
|
||||
*/
|
||||
public function register_adapter( string $name, Adapters\AdapterInterface $adapter ): void {
|
||||
$this->adapters[ $name ] = $adapter;
|
||||
Logger::debug( '注册 AI 适配器', array( 'name' => $name ) );
|
||||
}
|
||||
/**
|
||||
* 注册适配器
|
||||
*
|
||||
* @param string $name 适配器名称
|
||||
* @param Adapters\AdapterInterface $adapter 适配器实例
|
||||
*/
|
||||
public function register_adapter( string $name, Adapters\AdapterInterface $adapter ): void {
|
||||
$this->adapters[ $name ] = $adapter;
|
||||
Logger::debug( '注册 AI 适配器', [ 'name' => $name ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器
|
||||
*
|
||||
* @param string $name 适配器名称
|
||||
* @return Adapters\AdapterInterface|null
|
||||
*/
|
||||
public function get_adapter( string $name ): ?Adapters\AdapterInterface {
|
||||
return $this->adapters[ $name ] ?? null;
|
||||
}
|
||||
/**
|
||||
* 获取适配器
|
||||
*
|
||||
* @param string $name 适配器名称
|
||||
* @return Adapters\AdapterInterface|null
|
||||
*/
|
||||
public function get_adapter( string $name ): ?Adapters\AdapterInterface {
|
||||
return $this->adapters[ $name ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有适配器
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_adapters(): array {
|
||||
return $this->adapters;
|
||||
}
|
||||
/**
|
||||
* 获取所有适配器
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_adapters(): array {
|
||||
return $this->adapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态信息
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_status(): array {
|
||||
return array(
|
||||
'enabled' => $this->is_enabled(),
|
||||
'mode' => $this->get_mode(),
|
||||
'wpmind_available' => $this->is_wpmind_available(),
|
||||
'whitelist' => $this->whitelist,
|
||||
'adapters' => array_keys( $this->adapters ),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取状态信息
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_status(): array {
|
||||
return [
|
||||
'enabled' => $this->is_enabled(),
|
||||
'mode' => $this->get_mode(),
|
||||
'wpmind_available' => $this->is_wpmind_available(),
|
||||
'whitelist' => $this->whitelist,
|
||||
'adapters' => array_keys( $this->adapters ),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20,153 +20,153 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
abstract class AbstractAdapter implements AdapterInterface {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
protected Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
protected Settings $settings;
|
||||
|
||||
/**
|
||||
* 支持的插件 slug 列表
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $supported_plugins = array();
|
||||
/**
|
||||
* 支持的插件 slug 列表
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $supported_plugins = [];
|
||||
|
||||
/**
|
||||
* 匹配的 URL 模式
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $url_patterns = array();
|
||||
/**
|
||||
* 匹配的 URL 模式
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $url_patterns = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持该插件
|
||||
*
|
||||
* @param string $plugin_slug 插件 slug
|
||||
* @return bool
|
||||
*/
|
||||
public function supports( string $plugin_slug ): bool {
|
||||
return in_array( $plugin_slug, $this->supported_plugins, true );
|
||||
}
|
||||
/**
|
||||
* 检查是否支持该插件
|
||||
*
|
||||
* @param string $plugin_slug 插件 slug
|
||||
* @return bool
|
||||
*/
|
||||
public function supports( string $plugin_slug ): bool {
|
||||
return in_array( $plugin_slug, $this->supported_plugins, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查请求是否匹配
|
||||
*
|
||||
* @param string $url 请求 URL
|
||||
* @param array $args 请求参数
|
||||
* @return bool
|
||||
*/
|
||||
public function matches( string $url, array $args ): bool {
|
||||
foreach ( $this->url_patterns as $pattern ) {
|
||||
$result = @preg_match( $pattern, $url );
|
||||
if ( $result === false ) {
|
||||
$this->log( '无效的 URL 匹配模式', array( 'pattern' => $pattern ) );
|
||||
continue;
|
||||
}
|
||||
if ( $result === 1 ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 检查请求是否匹配
|
||||
*
|
||||
* @param string $url 请求 URL
|
||||
* @param array $args 请求参数
|
||||
* @return bool
|
||||
*/
|
||||
public function matches( string $url, array $args ): bool {
|
||||
foreach ( $this->url_patterns as $pattern ) {
|
||||
$result = @preg_match( $pattern, $url );
|
||||
if ( $result === false ) {
|
||||
$this->log( '无效的 URL 匹配模式', [ 'pattern' => $pattern ] );
|
||||
continue;
|
||||
}
|
||||
if ( $result === 1 ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool {
|
||||
$ai_settings = $this->settings->get( 'ai_bridge', array() );
|
||||
$adapters = $ai_settings['adapters'] ?? array();
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool {
|
||||
$ai_settings = $this->settings->get( 'ai_bridge', [] );
|
||||
$adapters = $ai_settings['adapters'] ?? [];
|
||||
|
||||
return in_array( $this->get_name(), $adapters, true );
|
||||
}
|
||||
return in_array( $this->get_name(), $adapters, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
*
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
protected function log( string $message, array $context = array() ): void {
|
||||
$context['adapter'] = $this->get_name();
|
||||
Logger::debug( $message, $context );
|
||||
}
|
||||
/**
|
||||
* 记录日志
|
||||
*
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
protected function log( string $message, array $context = [] ): void {
|
||||
$context['adapter'] = $this->get_name();
|
||||
Logger::debug( $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求体
|
||||
*
|
||||
* @param array $args 请求参数
|
||||
* @return array|null
|
||||
*/
|
||||
protected function get_request_body( array $args ): ?array {
|
||||
if ( empty( $args['body'] ) ) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 获取请求体
|
||||
*
|
||||
* @param array $args 请求参数
|
||||
* @return array|null
|
||||
*/
|
||||
protected function get_request_body( array $args ): ?array {
|
||||
if ( empty( $args['body'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$body = $args['body'];
|
||||
$body = $args['body'];
|
||||
|
||||
if ( is_string( $body ) ) {
|
||||
$decoded = json_decode( $body, true );
|
||||
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
|
||||
}
|
||||
if ( is_string( $body ) ) {
|
||||
$decoded = json_decode( $body, true );
|
||||
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
|
||||
}
|
||||
|
||||
return is_array( $body ) ? $body : null;
|
||||
}
|
||||
return is_array( $body ) ? $body : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求体
|
||||
*
|
||||
* @param array $args 请求参数
|
||||
* @param array $body 请求体
|
||||
* @return array
|
||||
*/
|
||||
protected function set_request_body( array $args, array $body ): array {
|
||||
$args['body'] = wp_json_encode( $body );
|
||||
return $args;
|
||||
}
|
||||
/**
|
||||
* 设置请求体
|
||||
*
|
||||
* @param array $args 请求参数
|
||||
* @param array $body 请求体
|
||||
* @return array
|
||||
*/
|
||||
protected function set_request_body( array $args, array $body ): array {
|
||||
$args['body'] = wp_json_encode( $body );
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应体
|
||||
*
|
||||
* @param array|\WP_Error $response 响应
|
||||
* @return array|null
|
||||
*/
|
||||
protected function get_response_body( $response ): ?array {
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 获取响应体
|
||||
*
|
||||
* @param array|\WP_Error $response 响应
|
||||
* @return array|null
|
||||
*/
|
||||
protected function get_response_body( $response ): ?array {
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
|
||||
if ( empty( $body ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $body ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = json_decode( $body, true );
|
||||
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
|
||||
}
|
||||
$decoded = json_decode( $body, true );
|
||||
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置响应体
|
||||
*
|
||||
* @param array $response 响应
|
||||
* @param array $body 响应体
|
||||
* @return array
|
||||
*/
|
||||
protected function set_response_body( array $response, array $body ): array {
|
||||
$response['body'] = wp_json_encode( $body );
|
||||
return $response;
|
||||
}
|
||||
/**
|
||||
* 设置响应体
|
||||
*
|
||||
* @param array $response 响应
|
||||
* @param array $body 响应体
|
||||
* @return array
|
||||
*/
|
||||
protected function set_response_body( array $response, array $body ): array {
|
||||
$response['body'] = wp_json_encode( $body );
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\AIBridge\Adapters;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,58 +17,58 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
interface AdapterInterface {
|
||||
|
||||
/**
|
||||
* 获取适配器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string;
|
||||
/**
|
||||
* 获取适配器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string;
|
||||
|
||||
/**
|
||||
* 获取适配器描述
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_description(): string;
|
||||
/**
|
||||
* 获取适配器描述
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_description(): string;
|
||||
|
||||
/**
|
||||
* 检查是否支持该插件
|
||||
*
|
||||
* @param string $plugin_slug 插件 slug
|
||||
* @return bool
|
||||
*/
|
||||
public function supports( string $plugin_slug ): bool;
|
||||
/**
|
||||
* 检查是否支持该插件
|
||||
*
|
||||
* @param string $plugin_slug 插件 slug
|
||||
* @return bool
|
||||
*/
|
||||
public function supports( string $plugin_slug ): bool;
|
||||
|
||||
/**
|
||||
* 检查请求是否匹配
|
||||
*
|
||||
* @param string $url 请求 URL
|
||||
* @param array $args 请求参数
|
||||
* @return bool
|
||||
*/
|
||||
public function matches( string $url, array $args ): bool;
|
||||
/**
|
||||
* 检查请求是否匹配
|
||||
*
|
||||
* @param string $url 请求 URL
|
||||
* @param array $args 请求参数
|
||||
* @return bool
|
||||
*/
|
||||
public function matches( string $url, array $args ): bool;
|
||||
|
||||
/**
|
||||
* 转换请求
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 原始参数
|
||||
* @return array [url, args]
|
||||
*/
|
||||
public function transform_request( string $url, array $args ): array;
|
||||
/**
|
||||
* 转换请求
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 原始参数
|
||||
* @return array [url, args]
|
||||
*/
|
||||
public function transform_request( string $url, array $args ): array;
|
||||
|
||||
/**
|
||||
* 转换响应
|
||||
*
|
||||
* @param array|\WP_Error $response 原始响应
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function transform_response( $response );
|
||||
/**
|
||||
* 转换响应
|
||||
*
|
||||
* @param array|\WP_Error $response 原始响应
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function transform_response( $response );
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool;
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\AIBridge\Adapters;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -18,174 +18,168 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class RankMathAdapter extends AbstractAdapter {
|
||||
|
||||
/**
|
||||
* 支持的插件
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $supported_plugins = array(
|
||||
'seo-by-rank-math',
|
||||
'seo-by-rank-math-pro',
|
||||
);
|
||||
/**
|
||||
* 支持的插件
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $supported_plugins = [
|
||||
'seo-by-rank-math',
|
||||
'seo-by-rank-math-pro',
|
||||
];
|
||||
|
||||
/**
|
||||
* URL 匹配模式
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $url_patterns = array(
|
||||
'#api\.openai\.com/v1/chat/completions#',
|
||||
'#rankmath\.com.*api#i',
|
||||
'#content-ai\.rankmath\.com#i',
|
||||
);
|
||||
/**
|
||||
* URL 匹配模式
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $url_patterns = [
|
||||
'#api\.openai\.com/v1/chat/completions#',
|
||||
'#rankmath\.com.*api#i',
|
||||
'#content-ai\.rankmath\.com#i',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取适配器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'rankmath';
|
||||
}
|
||||
/**
|
||||
* 获取适配器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'rankmath';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器描述
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return __( 'Rank Math Content AI 功能适配', 'wpbridge' );
|
||||
}
|
||||
/**
|
||||
* 获取适配器描述
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return __( 'Rank Math Content AI 功能适配', 'wpbridge' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换请求
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 原始参数
|
||||
* @return array [url, args]
|
||||
*/
|
||||
public function transform_request( string $url, array $args ): array {
|
||||
$body = $this->get_request_body( $args );
|
||||
/**
|
||||
* 转换请求
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 原始参数
|
||||
* @return array [url, args]
|
||||
*/
|
||||
public function transform_request( string $url, array $args ): array {
|
||||
$body = $this->get_request_body( $args );
|
||||
|
||||
if ( null === $body ) {
|
||||
return array( $url, $args );
|
||||
}
|
||||
if ( null === $body ) {
|
||||
return [ $url, $args ];
|
||||
}
|
||||
|
||||
$this->log(
|
||||
'Rank Math AI 请求转换',
|
||||
array(
|
||||
'model' => $body['model'] ?? 'unknown',
|
||||
'type' => $this->detect_request_type( $body ),
|
||||
)
|
||||
);
|
||||
$this->log( 'Rank Math AI 请求转换', [
|
||||
'model' => $body['model'] ?? 'unknown',
|
||||
'type' => $this->detect_request_type( $body ),
|
||||
] );
|
||||
|
||||
// 转换模型名称
|
||||
if ( isset( $body['model'] ) ) {
|
||||
$body['model'] = $this->map_model( $body['model'] );
|
||||
}
|
||||
// 转换模型名称
|
||||
if ( isset( $body['model'] ) ) {
|
||||
$body['model'] = $this->map_model( $body['model'] );
|
||||
}
|
||||
|
||||
// 根据请求类型优化
|
||||
$request_type = $this->detect_request_type( $body );
|
||||
$body = $this->optimize_for_type( $body, $request_type );
|
||||
// 根据请求类型优化
|
||||
$request_type = $this->detect_request_type( $body );
|
||||
$body = $this->optimize_for_type( $body, $request_type );
|
||||
|
||||
$args = $this->set_request_body( $args, $body );
|
||||
$args = $this->set_request_body( $args, $body );
|
||||
|
||||
return array( $url, $args );
|
||||
}
|
||||
return [ $url, $args ];
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换响应
|
||||
*
|
||||
* @param array|\WP_Error $response 原始响应
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function transform_response( $response ) {
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
/**
|
||||
* 转换响应
|
||||
*
|
||||
* @param array|\WP_Error $response 原始响应
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function transform_response( $response ) {
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = $this->get_response_body( $response );
|
||||
$body = $this->get_response_body( $response );
|
||||
|
||||
if ( null === $body ) {
|
||||
return $response;
|
||||
}
|
||||
if ( null === $body ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->log(
|
||||
'Rank Math AI 响应转换',
|
||||
array(
|
||||
'has_choices' => isset( $body['choices'] ),
|
||||
)
|
||||
);
|
||||
$this->log( 'Rank Math AI 响应转换', [
|
||||
'has_choices' => isset( $body['choices'] ),
|
||||
] );
|
||||
|
||||
return $response;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测请求类型
|
||||
*
|
||||
* @param array $body 请求体
|
||||
* @return string
|
||||
*/
|
||||
private function detect_request_type( array $body ): string {
|
||||
if ( ! isset( $body['messages'] ) || ! is_array( $body['messages'] ) ) {
|
||||
return 'unknown';
|
||||
}
|
||||
/**
|
||||
* 检测请求类型
|
||||
*
|
||||
* @param array $body 请求体
|
||||
* @return string
|
||||
*/
|
||||
private function detect_request_type( array $body ): string {
|
||||
if ( ! isset( $body['messages'] ) || ! is_array( $body['messages'] ) ) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
$content = '';
|
||||
foreach ( $body['messages'] as $message ) {
|
||||
if ( isset( $message['content'] ) ) {
|
||||
$content .= $message['content'] . ' ';
|
||||
}
|
||||
}
|
||||
$content = '';
|
||||
foreach ( $body['messages'] as $message ) {
|
||||
if ( isset( $message['content'] ) ) {
|
||||
$content .= $message['content'] . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
$content = strtolower( $content );
|
||||
$content = strtolower( $content );
|
||||
|
||||
if ( strpos( $content, 'title' ) !== false || strpos( $content, '标题' ) !== false ) {
|
||||
return 'title';
|
||||
}
|
||||
if ( strpos( $content, 'title' ) !== false || strpos( $content, '标题' ) !== false ) {
|
||||
return 'title';
|
||||
}
|
||||
|
||||
if ( strpos( $content, 'description' ) !== false || strpos( $content, '描述' ) !== false ) {
|
||||
return 'description';
|
||||
}
|
||||
if ( strpos( $content, 'description' ) !== false || strpos( $content, '描述' ) !== false ) {
|
||||
return 'description';
|
||||
}
|
||||
|
||||
if ( strpos( $content, 'keyword' ) !== false || strpos( $content, '关键词' ) !== false ) {
|
||||
return 'keyword';
|
||||
}
|
||||
if ( strpos( $content, 'keyword' ) !== false || strpos( $content, '关键词' ) !== false ) {
|
||||
return 'keyword';
|
||||
}
|
||||
|
||||
if ( strpos( $content, 'content' ) !== false || strpos( $content, '内容' ) !== false ) {
|
||||
return 'content';
|
||||
}
|
||||
if ( strpos( $content, 'content' ) !== false || strpos( $content, '内容' ) !== false ) {
|
||||
return 'content';
|
||||
}
|
||||
|
||||
return 'general';
|
||||
}
|
||||
return 'general';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型优化请求
|
||||
*
|
||||
* @param array $body 请求体
|
||||
* @param string $type 请求类型
|
||||
* @return array
|
||||
*/
|
||||
private function optimize_for_type( array $body, string $type ): array {
|
||||
// 可以根据不同类型添加优化逻辑
|
||||
// 例如:为中文内容生成添加特定提示
|
||||
/**
|
||||
* 根据类型优化请求
|
||||
*
|
||||
* @param array $body 请求体
|
||||
* @param string $type 请求类型
|
||||
* @return array
|
||||
*/
|
||||
private function optimize_for_type( array $body, string $type ): array {
|
||||
// 可以根据不同类型添加优化逻辑
|
||||
// 例如:为中文内容生成添加特定提示
|
||||
|
||||
return $body;
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射模型名称
|
||||
*
|
||||
* @param string $model 原始模型名称
|
||||
* @return string
|
||||
*/
|
||||
private function map_model( string $model ): string {
|
||||
$model_map = array(
|
||||
'gpt-4' => 'gpt-4',
|
||||
'gpt-4-turbo' => 'gpt-4-turbo',
|
||||
'gpt-3.5-turbo' => 'gpt-3.5-turbo',
|
||||
);
|
||||
/**
|
||||
* 映射模型名称
|
||||
*
|
||||
* @param string $model 原始模型名称
|
||||
* @return string
|
||||
*/
|
||||
private function map_model( string $model ): string {
|
||||
$model_map = [
|
||||
'gpt-4' => 'gpt-4',
|
||||
'gpt-4-turbo' => 'gpt-4-turbo',
|
||||
'gpt-3.5-turbo' => 'gpt-3.5-turbo',
|
||||
];
|
||||
|
||||
return $model_map[ $model ] ?? $model;
|
||||
}
|
||||
return $model_map[ $model ] ?? $model;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\AIBridge\Adapters;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -18,134 +18,128 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class YoastAdapter extends AbstractAdapter {
|
||||
|
||||
/**
|
||||
* 支持的插件
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $supported_plugins = array(
|
||||
'wordpress-seo-premium',
|
||||
'wordpress-seo',
|
||||
);
|
||||
/**
|
||||
* 支持的插件
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $supported_plugins = [
|
||||
'wordpress-seo-premium',
|
||||
'wordpress-seo',
|
||||
];
|
||||
|
||||
/**
|
||||
* URL 匹配模式
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $url_patterns = array(
|
||||
'#api\.openai\.com/v1/chat/completions#',
|
||||
'#yoast\.com.*ai#i',
|
||||
);
|
||||
/**
|
||||
* URL 匹配模式
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $url_patterns = [
|
||||
'#api\.openai\.com/v1/chat/completions#',
|
||||
'#yoast\.com.*ai#i',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取适配器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'yoast';
|
||||
}
|
||||
/**
|
||||
* 获取适配器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'yoast';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器描述
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return __( 'Yoast SEO Premium AI 功能适配', 'wpbridge' );
|
||||
}
|
||||
/**
|
||||
* 获取适配器描述
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return __( 'Yoast SEO Premium AI 功能适配', 'wpbridge' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换请求
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 原始参数
|
||||
* @return array [url, args]
|
||||
*/
|
||||
public function transform_request( string $url, array $args ): array {
|
||||
$body = $this->get_request_body( $args );
|
||||
/**
|
||||
* 转换请求
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @param array $args 原始参数
|
||||
* @return array [url, args]
|
||||
*/
|
||||
public function transform_request( string $url, array $args ): array {
|
||||
$body = $this->get_request_body( $args );
|
||||
|
||||
if ( null === $body ) {
|
||||
return array( $url, $args );
|
||||
}
|
||||
if ( null === $body ) {
|
||||
return [ $url, $args ];
|
||||
}
|
||||
|
||||
$this->log(
|
||||
'Yoast AI 请求转换',
|
||||
array(
|
||||
'model' => $body['model'] ?? 'unknown',
|
||||
)
|
||||
);
|
||||
$this->log( 'Yoast AI 请求转换', [
|
||||
'model' => $body['model'] ?? 'unknown',
|
||||
] );
|
||||
|
||||
// 转换模型名称(如果需要)
|
||||
if ( isset( $body['model'] ) ) {
|
||||
$body['model'] = $this->map_model( $body['model'] );
|
||||
}
|
||||
// 转换模型名称(如果需要)
|
||||
if ( isset( $body['model'] ) ) {
|
||||
$body['model'] = $this->map_model( $body['model'] );
|
||||
}
|
||||
|
||||
// 添加系统提示优化
|
||||
if ( isset( $body['messages'] ) && is_array( $body['messages'] ) ) {
|
||||
$body['messages'] = $this->optimize_messages( $body['messages'] );
|
||||
}
|
||||
// 添加系统提示优化
|
||||
if ( isset( $body['messages'] ) && is_array( $body['messages'] ) ) {
|
||||
$body['messages'] = $this->optimize_messages( $body['messages'] );
|
||||
}
|
||||
|
||||
$args = $this->set_request_body( $args, $body );
|
||||
$args = $this->set_request_body( $args, $body );
|
||||
|
||||
return array( $url, $args );
|
||||
}
|
||||
return [ $url, $args ];
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换响应
|
||||
*
|
||||
* @param array|\WP_Error $response 原始响应
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function transform_response( $response ) {
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
/**
|
||||
* 转换响应
|
||||
*
|
||||
* @param array|\WP_Error $response 原始响应
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function transform_response( $response ) {
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$body = $this->get_response_body( $response );
|
||||
$body = $this->get_response_body( $response );
|
||||
|
||||
if ( null === $body ) {
|
||||
return $response;
|
||||
}
|
||||
if ( null === $body ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->log(
|
||||
'Yoast AI 响应转换',
|
||||
array(
|
||||
'has_choices' => isset( $body['choices'] ),
|
||||
)
|
||||
);
|
||||
$this->log( 'Yoast AI 响应转换', [
|
||||
'has_choices' => isset( $body['choices'] ),
|
||||
] );
|
||||
|
||||
// 响应格式通常兼容,无需转换
|
||||
return $response;
|
||||
}
|
||||
// 响应格式通常兼容,无需转换
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射模型名称
|
||||
*
|
||||
* @param string $model 原始模型名称
|
||||
* @return string
|
||||
*/
|
||||
private function map_model( string $model ): string {
|
||||
$model_map = array(
|
||||
'gpt-4' => 'gpt-4',
|
||||
'gpt-4-turbo' => 'gpt-4-turbo',
|
||||
'gpt-3.5-turbo' => 'gpt-3.5-turbo',
|
||||
);
|
||||
/**
|
||||
* 映射模型名称
|
||||
*
|
||||
* @param string $model 原始模型名称
|
||||
* @return string
|
||||
*/
|
||||
private function map_model( string $model ): string {
|
||||
$model_map = [
|
||||
'gpt-4' => 'gpt-4',
|
||||
'gpt-4-turbo' => 'gpt-4-turbo',
|
||||
'gpt-3.5-turbo' => 'gpt-3.5-turbo',
|
||||
];
|
||||
|
||||
return $model_map[ $model ] ?? $model;
|
||||
}
|
||||
return $model_map[ $model ] ?? $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化消息
|
||||
*
|
||||
* @param array $messages 消息列表
|
||||
* @return array
|
||||
*/
|
||||
private function optimize_messages( array $messages ): array {
|
||||
// 可以在这里添加中文优化提示
|
||||
// 例如:添加系统消息要求返回中文内容
|
||||
/**
|
||||
* 优化消息
|
||||
*
|
||||
* @param array $messages 消息列表
|
||||
* @return array
|
||||
*/
|
||||
private function optimize_messages( array $messages ): array {
|
||||
// 可以在这里添加中文优化提示
|
||||
// 例如:添加系统消息要求返回中文内容
|
||||
|
||||
return $messages;
|
||||
}
|
||||
return $messages;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use WPBridge\Security\Encryption;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -21,276 +21,267 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class ApiKeyManager {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成新的 API Key
|
||||
*
|
||||
* @param string $name Key 名称
|
||||
* @param string|null $expires_at 过期时间
|
||||
* @param array $permissions 权限列表
|
||||
* @return array
|
||||
*/
|
||||
public function generate( string $name, ?string $expires_at = null, array $permissions = array() ): array {
|
||||
// 权限检查
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
throw new \Exception( __( '权限不足', 'wpbridge' ) );
|
||||
}
|
||||
/**
|
||||
* 生成新的 API Key
|
||||
*
|
||||
* @param string $name Key 名称
|
||||
* @param string|null $expires_at 过期时间
|
||||
* @param array $permissions 权限列表
|
||||
* @return array
|
||||
*/
|
||||
public function generate( string $name, ?string $expires_at = null, array $permissions = [] ): array {
|
||||
// 权限检查
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
throw new \Exception( __( '权限不足', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
$api_key = Encryption::generate_token( 32 );
|
||||
$key_id = 'key_' . wp_generate_uuid4();
|
||||
$api_key = Encryption::generate_token( 32 );
|
||||
$key_id = 'key_' . wp_generate_uuid4();
|
||||
|
||||
$key_data = array(
|
||||
'id' => $key_id,
|
||||
'name' => sanitize_text_field( $name ),
|
||||
'key_hash' => password_hash( $api_key, PASSWORD_DEFAULT ), // 存储哈希而非明文
|
||||
'key_prefix' => substr( $api_key, 0, 4 ) . '...' . substr( $api_key, -4 ), // 显示前4后4
|
||||
'permissions' => $permissions,
|
||||
'expires_at' => $expires_at,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'created_by' => get_current_user_id(),
|
||||
'last_used' => null,
|
||||
'usage_count' => 0,
|
||||
);
|
||||
$key_data = [
|
||||
'id' => $key_id,
|
||||
'name' => sanitize_text_field( $name ),
|
||||
'key_hash' => password_hash( $api_key, PASSWORD_DEFAULT ), // 存储哈希而非明文
|
||||
'key_prefix' => substr( $api_key, 0, 4 ) . '...' . substr( $api_key, -4 ), // 显示前4后4
|
||||
'permissions' => $permissions,
|
||||
'expires_at' => $expires_at,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'created_by' => get_current_user_id(),
|
||||
'last_used' => null,
|
||||
'usage_count' => 0,
|
||||
];
|
||||
|
||||
// 保存到设置
|
||||
$api_settings = $this->settings->get( 'api', array() );
|
||||
$api_settings['keys'] = $api_settings['keys'] ?? array();
|
||||
$api_settings['keys'][] = $key_data;
|
||||
// 保存到设置
|
||||
$api_settings = $this->settings->get( 'api', [] );
|
||||
$api_settings['keys'] = $api_settings['keys'] ?? [];
|
||||
$api_settings['keys'][] = $key_data;
|
||||
|
||||
$this->settings->set( 'api', $api_settings );
|
||||
$this->settings->set( 'api', $api_settings );
|
||||
|
||||
Logger::info(
|
||||
'API Key 已创建',
|
||||
array(
|
||||
'id' => $key_id,
|
||||
'name' => $name,
|
||||
)
|
||||
);
|
||||
Logger::info( 'API Key 已创建', [ 'id' => $key_id, 'name' => $name ] );
|
||||
|
||||
return array(
|
||||
'id' => $key_id,
|
||||
'name' => $name,
|
||||
'key' => $api_key, // 只在创建时返回完整 key
|
||||
'expires_at' => $expires_at,
|
||||
'created_at' => $key_data['created_at'],
|
||||
);
|
||||
}
|
||||
return [
|
||||
'id' => $key_id,
|
||||
'name' => $name,
|
||||
'key' => $api_key, // 只在创建时返回完整 key
|
||||
'expires_at' => $expires_at,
|
||||
'created_at' => $key_data['created_at'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 API Keys(不含完整 key)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
$api_settings = $this->settings->get( 'api', array() );
|
||||
$keys = $api_settings['keys'] ?? array();
|
||||
/**
|
||||
* 获取所有 API Keys(不含完整 key)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
$api_settings = $this->settings->get( 'api', [] );
|
||||
$keys = $api_settings['keys'] ?? [];
|
||||
|
||||
return array_map(
|
||||
function ( $key ) {
|
||||
return array(
|
||||
'id' => $key['id'],
|
||||
'name' => $key['name'],
|
||||
'key_prefix' => $key['key_prefix'],
|
||||
'permissions' => $key['permissions'] ?? array(),
|
||||
'expires_at' => $key['expires_at'],
|
||||
'created_at' => $key['created_at'],
|
||||
'last_used' => $key['last_used'],
|
||||
'usage_count' => $key['usage_count'] ?? 0,
|
||||
'is_expired' => $this->is_expired( $key ),
|
||||
);
|
||||
},
|
||||
$keys
|
||||
);
|
||||
}
|
||||
return array_map( function ( $key ) {
|
||||
return [
|
||||
'id' => $key['id'],
|
||||
'name' => $key['name'],
|
||||
'key_prefix' => $key['key_prefix'],
|
||||
'permissions' => $key['permissions'] ?? [],
|
||||
'expires_at' => $key['expires_at'],
|
||||
'created_at' => $key['created_at'],
|
||||
'last_used' => $key['last_used'],
|
||||
'usage_count' => $key['usage_count'] ?? 0,
|
||||
'is_expired' => $this->is_expired( $key ),
|
||||
];
|
||||
}, $keys );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个 API Key
|
||||
*
|
||||
* @param string $key_id Key ID
|
||||
* @return array|null
|
||||
*/
|
||||
public function get( string $key_id ): ?array {
|
||||
$api_settings = $this->settings->get( 'api', array() );
|
||||
$keys = $api_settings['keys'] ?? array();
|
||||
/**
|
||||
* 获取单个 API Key
|
||||
*
|
||||
* @param string $key_id Key ID
|
||||
* @return array|null
|
||||
*/
|
||||
public function get( string $key_id ): ?array {
|
||||
$api_settings = $this->settings->get( 'api', [] );
|
||||
$keys = $api_settings['keys'] ?? [];
|
||||
|
||||
foreach ( $keys as $key ) {
|
||||
if ( $key['id'] === $key_id ) {
|
||||
return array(
|
||||
'id' => $key['id'],
|
||||
'name' => $key['name'],
|
||||
'key_prefix' => $key['key_prefix'],
|
||||
'permissions' => $key['permissions'] ?? array(),
|
||||
'expires_at' => $key['expires_at'],
|
||||
'created_at' => $key['created_at'],
|
||||
'last_used' => $key['last_used'],
|
||||
'usage_count' => $key['usage_count'] ?? 0,
|
||||
'is_expired' => $this->is_expired( $key ),
|
||||
);
|
||||
}
|
||||
}
|
||||
foreach ( $keys as $key ) {
|
||||
if ( $key['id'] === $key_id ) {
|
||||
return [
|
||||
'id' => $key['id'],
|
||||
'name' => $key['name'],
|
||||
'key_prefix' => $key['key_prefix'],
|
||||
'permissions' => $key['permissions'] ?? [],
|
||||
'expires_at' => $key['expires_at'],
|
||||
'created_at' => $key['created_at'],
|
||||
'last_used' => $key['last_used'],
|
||||
'usage_count' => $key['usage_count'] ?? 0,
|
||||
'is_expired' => $this->is_expired( $key ),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 API Key
|
||||
*
|
||||
* @param string $key_id Key ID
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $key_id ): bool {
|
||||
$api_settings = $this->settings->get( 'api', array() );
|
||||
$keys = $api_settings['keys'] ?? array();
|
||||
$new_keys = array();
|
||||
/**
|
||||
* 删除 API Key
|
||||
*
|
||||
* @param string $key_id Key ID
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $key_id ): bool {
|
||||
$api_settings = $this->settings->get( 'api', [] );
|
||||
$keys = $api_settings['keys'] ?? [];
|
||||
$new_keys = [];
|
||||
|
||||
foreach ( $keys as $key ) {
|
||||
if ( $key['id'] !== $key_id ) {
|
||||
$new_keys[] = $key;
|
||||
}
|
||||
}
|
||||
foreach ( $keys as $key ) {
|
||||
if ( $key['id'] !== $key_id ) {
|
||||
$new_keys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $new_keys ) === count( $keys ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( count( $new_keys ) === count( $keys ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$api_settings['keys'] = $new_keys;
|
||||
$this->settings->set( 'api', $api_settings );
|
||||
$api_settings['keys'] = $new_keys;
|
||||
$this->settings->set( 'api', $api_settings );
|
||||
|
||||
Logger::info( 'API Key 已删除', array( 'id' => $key_id ) );
|
||||
Logger::info( 'API Key 已删除', [ 'id' => $key_id ] );
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 API Key
|
||||
*
|
||||
* @param string $key_id Key ID
|
||||
* @param array $data 更新数据
|
||||
* @return bool
|
||||
*/
|
||||
public function update( string $key_id, array $data ): bool {
|
||||
$api_settings = $this->settings->get( 'api', array() );
|
||||
$keys = $api_settings['keys'] ?? array();
|
||||
$found = false;
|
||||
/**
|
||||
* 更新 API Key
|
||||
*
|
||||
* @param string $key_id Key ID
|
||||
* @param array $data 更新数据
|
||||
* @return bool
|
||||
*/
|
||||
public function update( string $key_id, array $data ): bool {
|
||||
$api_settings = $this->settings->get( 'api', [] );
|
||||
$keys = $api_settings['keys'] ?? [];
|
||||
$found = false;
|
||||
|
||||
foreach ( $keys as $index => $key ) {
|
||||
if ( $key['id'] === $key_id ) {
|
||||
if ( isset( $data['name'] ) ) {
|
||||
$keys[ $index ]['name'] = sanitize_text_field( $data['name'] );
|
||||
}
|
||||
if ( isset( $data['expires_at'] ) ) {
|
||||
$keys[ $index ]['expires_at'] = $data['expires_at'];
|
||||
}
|
||||
if ( isset( $data['permissions'] ) ) {
|
||||
$keys[ $index ]['permissions'] = $data['permissions'];
|
||||
}
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach ( $keys as $index => $key ) {
|
||||
if ( $key['id'] === $key_id ) {
|
||||
if ( isset( $data['name'] ) ) {
|
||||
$keys[ $index ]['name'] = sanitize_text_field( $data['name'] );
|
||||
}
|
||||
if ( isset( $data['expires_at'] ) ) {
|
||||
$keys[ $index ]['expires_at'] = $data['expires_at'];
|
||||
}
|
||||
if ( isset( $data['permissions'] ) ) {
|
||||
$keys[ $index ]['permissions'] = $data['permissions'];
|
||||
}
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $found ) {
|
||||
return false;
|
||||
}
|
||||
if ( ! $found ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$api_settings['keys'] = $keys;
|
||||
$this->settings->set( 'api', $api_settings );
|
||||
$api_settings['keys'] = $keys;
|
||||
$this->settings->set( 'api', $api_settings );
|
||||
|
||||
Logger::info( 'API Key 已更新', array( 'id' => $key_id ) );
|
||||
Logger::info( 'API Key 已更新', [ 'id' => $key_id ] );
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 API Key 使用
|
||||
*
|
||||
* @param string $api_key API Key
|
||||
*/
|
||||
public function record_usage( string $api_key ): void {
|
||||
$api_settings = $this->settings->get( 'api', array() );
|
||||
$keys = $api_settings['keys'] ?? array();
|
||||
/**
|
||||
* 记录 API Key 使用
|
||||
*
|
||||
* @param string $api_key API Key
|
||||
*/
|
||||
public function record_usage( string $api_key ): void {
|
||||
$api_settings = $this->settings->get( 'api', [] );
|
||||
$keys = $api_settings['keys'] ?? [];
|
||||
|
||||
foreach ( $keys as $index => $key ) {
|
||||
if ( isset( $key['key_hash'] ) && password_verify( $api_key, $key['key_hash'] ) ) {
|
||||
$keys[ $index ]['last_used'] = current_time( 'mysql' );
|
||||
$keys[ $index ]['usage_count'] = ( $key['usage_count'] ?? 0 ) + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach ( $keys as $index => $key ) {
|
||||
if ( isset( $key['key_hash'] ) && password_verify( $api_key, $key['key_hash'] ) ) {
|
||||
$keys[ $index ]['last_used'] = current_time( 'mysql' );
|
||||
$keys[ $index ]['usage_count'] = ( $key['usage_count'] ?? 0 ) + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$api_settings['keys'] = $keys;
|
||||
$this->settings->set( 'api', $api_settings );
|
||||
}
|
||||
$api_settings['keys'] = $keys;
|
||||
$this->settings->set( 'api', $api_settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Key 是否过期
|
||||
*
|
||||
* @param array $key Key 数据
|
||||
* @return bool
|
||||
*/
|
||||
private function is_expired( array $key ): bool {
|
||||
if ( empty( $key['expires_at'] ) ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 检查 Key 是否过期
|
||||
*
|
||||
* @param array $key Key 数据
|
||||
* @return bool
|
||||
*/
|
||||
private function is_expired( array $key ): bool {
|
||||
if ( empty( $key['expires_at'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strtotime( $key['expires_at'] ) < time();
|
||||
}
|
||||
return strtotime( $key['expires_at'] ) < time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销所有 API Keys
|
||||
*
|
||||
* @return int 撤销的数量
|
||||
*/
|
||||
public function revoke_all(): int {
|
||||
$api_settings = $this->settings->get( 'api', array() );
|
||||
$count = count( $api_settings['keys'] ?? array() );
|
||||
/**
|
||||
* 撤销所有 API Keys
|
||||
*
|
||||
* @return int 撤销的数量
|
||||
*/
|
||||
public function revoke_all(): int {
|
||||
$api_settings = $this->settings->get( 'api', [] );
|
||||
$count = count( $api_settings['keys'] ?? [] );
|
||||
|
||||
$api_settings['keys'] = array();
|
||||
$this->settings->set( 'api', $api_settings );
|
||||
$api_settings['keys'] = [];
|
||||
$this->settings->set( 'api', $api_settings );
|
||||
|
||||
Logger::info( '所有 API Keys 已撤销', array( 'count' => $count ) );
|
||||
Logger::info( '所有 API Keys 已撤销', [ 'count' => $count ] );
|
||||
|
||||
return $count;
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
$keys = $this->get_all();
|
||||
$total = count( $keys );
|
||||
$active = 0;
|
||||
$expired = 0;
|
||||
/**
|
||||
* 获取统计信息
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
$keys = $this->get_all();
|
||||
$total = count( $keys );
|
||||
$active = 0;
|
||||
$expired = 0;
|
||||
|
||||
foreach ( $keys as $key ) {
|
||||
if ( $key['is_expired'] ) {
|
||||
++$expired;
|
||||
} else {
|
||||
++$active;
|
||||
}
|
||||
}
|
||||
foreach ( $keys as $key ) {
|
||||
if ( $key['is_expired'] ) {
|
||||
$expired++;
|
||||
} else {
|
||||
$active++;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'total' => $total,
|
||||
'active' => $active,
|
||||
'expired' => $expired,
|
||||
);
|
||||
}
|
||||
return [
|
||||
'total' => $total,
|
||||
'active' => $active,
|
||||
'expired' => $expired,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -55,18 +55,18 @@ class VendorAdmin {
|
|||
*/
|
||||
private function init_hooks(): void {
|
||||
// 供应商 AJAX 处理
|
||||
add_action( 'wp_ajax_wpbridge_add_vendor', array( $this, 'ajax_add_vendor' ) );
|
||||
add_action( 'wp_ajax_wpbridge_remove_vendor', array( $this, 'ajax_remove_vendor' ) );
|
||||
add_action( 'wp_ajax_wpbridge_test_vendor', array( $this, 'ajax_test_vendor' ) );
|
||||
add_action( 'wp_ajax_wpbridge_toggle_vendor', array( $this, 'ajax_toggle_vendor' ) );
|
||||
add_action( 'wp_ajax_wpbridge_sync_vendor_plugins', array( $this, 'ajax_sync_vendor_plugins' ) );
|
||||
add_action( 'wp_ajax_wpbridge_add_vendor', [ $this, 'ajax_add_vendor' ] );
|
||||
add_action( 'wp_ajax_wpbridge_remove_vendor', [ $this, 'ajax_remove_vendor' ] );
|
||||
add_action( 'wp_ajax_wpbridge_test_vendor', [ $this, 'ajax_test_vendor' ] );
|
||||
add_action( 'wp_ajax_wpbridge_toggle_vendor', [ $this, 'ajax_toggle_vendor' ] );
|
||||
add_action( 'wp_ajax_wpbridge_sync_vendor_plugins', [ $this, 'ajax_sync_vendor_plugins' ] );
|
||||
|
||||
// 自定义插件 AJAX 处理
|
||||
add_action( 'wp_ajax_wpbridge_add_custom_plugin', array( $this, 'ajax_add_custom_plugin' ) );
|
||||
add_action( 'wp_ajax_wpbridge_remove_custom_plugin', array( $this, 'ajax_remove_custom_plugin' ) );
|
||||
add_action( 'wp_ajax_wpbridge_add_custom_plugin', [ $this, 'ajax_add_custom_plugin' ] );
|
||||
add_action( 'wp_ajax_wpbridge_remove_custom_plugin', [ $this, 'ajax_remove_custom_plugin' ] );
|
||||
|
||||
// Bridge Server AJAX 处理
|
||||
add_action( 'wp_ajax_wpbridge_test_bridge_server', array( $this, 'ajax_test_bridge_server' ) );
|
||||
add_action( 'wp_ajax_wpbridge_test_bridge_server', [ $this, 'ajax_test_bridge_server' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -76,7 +76,7 @@ class VendorAdmin {
|
|||
*/
|
||||
private function get_bridge_manager(): BridgeManager {
|
||||
if ( null === $this->bridge_manager ) {
|
||||
$remote_config = new \WPBridge\Core\RemoteConfig( $this->settings );
|
||||
$remote_config = new \WPBridge\Core\RemoteConfig( $this->settings );
|
||||
$this->bridge_manager = new BridgeManager( $this->settings, $remote_config );
|
||||
}
|
||||
return $this->bridge_manager;
|
||||
|
|
@ -91,7 +91,7 @@ class VendorAdmin {
|
|||
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
|
||||
|
|
@ -103,27 +103,27 @@ class VendorAdmin {
|
|||
|
||||
// 验证必填字段
|
||||
if ( empty( $vendor_id ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
if ( empty( $name ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '供应商名称不能为空', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '供应商名称不能为空', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
if ( empty( $api_url ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'API 地址不能为空', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( 'API 地址不能为空', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
// 验证 URL 协议
|
||||
$scheme = wp_parse_url( $api_url, PHP_URL_SCHEME );
|
||||
if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'API 地址必须使用 http 或 https 协议', 'wpbridge' ) ) );
|
||||
if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
|
||||
wp_send_json_error( [ 'message' => __( 'API 地址必须使用 http 或 https 协议', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
// 验证供应商类型
|
||||
$allowed_types = array( 'woocommerce' );
|
||||
$allowed_types = [ 'woocommerce' ];
|
||||
if ( ! in_array( $type, $allowed_types, true ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '不支持的供应商类型', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '不支持的供应商类型', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$result = $this->get_bridge_manager()->add_vendor(
|
||||
|
|
@ -151,13 +151,13 @@ class VendorAdmin {
|
|||
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
|
||||
|
||||
if ( empty( $vendor_id ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$result = $this->get_bridge_manager()->remove_vendor( $vendor_id );
|
||||
|
|
@ -178,13 +178,13 @@ class VendorAdmin {
|
|||
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
|
||||
|
||||
if ( empty( $vendor_id ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$result = $this->get_bridge_manager()->test_vendor_connection( $vendor_id );
|
||||
|
|
@ -205,40 +205,35 @@ class VendorAdmin {
|
|||
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
|
||||
$enabled = ! empty( $_POST['enabled'] );
|
||||
|
||||
if ( empty( $vendor_id ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$vendors = $this->settings->get( 'vendors', array() );
|
||||
$vendors = $this->settings->get( 'vendors', [] );
|
||||
|
||||
if ( ! isset( $vendors[ $vendor_id ] ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '供应商不存在', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '供应商不存在', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$vendors[ $vendor_id ]['enabled'] = $enabled;
|
||||
$this->settings->set( 'vendors', $vendors );
|
||||
|
||||
Logger::info(
|
||||
'Vendor toggled',
|
||||
array(
|
||||
'vendor_id' => $vendor_id,
|
||||
'enabled' => $enabled,
|
||||
)
|
||||
);
|
||||
Logger::info( 'Vendor toggled', [
|
||||
'vendor_id' => $vendor_id,
|
||||
'enabled' => $enabled,
|
||||
] );
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => $enabled
|
||||
? __( '供应商已启用', 'wpbridge' )
|
||||
: __( '供应商已禁用', 'wpbridge' ),
|
||||
)
|
||||
);
|
||||
wp_send_json_success( [
|
||||
'message' => $enabled
|
||||
? __( '供应商已启用', 'wpbridge' )
|
||||
: __( '供应商已禁用', 'wpbridge' ),
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -250,7 +245,7 @@ class VendorAdmin {
|
|||
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
|
||||
|
|
@ -261,33 +256,29 @@ class VendorAdmin {
|
|||
// 同步单个供应商
|
||||
$vendor = $vendor_manager->get( $vendor_id );
|
||||
if ( ! $vendor ) {
|
||||
wp_send_json_error( array( 'message' => __( '供应商不存在', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '供应商不存在', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$plugins = $vendor->get_plugins( true ); // 强制刷新
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => sprintf(
|
||||
wp_send_json_success( [
|
||||
'message' => sprintf(
|
||||
/* translators: %d: plugin count */
|
||||
__( '已同步 %d 个插件', 'wpbridge' ),
|
||||
count( $plugins )
|
||||
),
|
||||
'count' => count( $plugins ),
|
||||
)
|
||||
);
|
||||
__( '已同步 %d 个插件', 'wpbridge' ),
|
||||
count( $plugins )
|
||||
),
|
||||
'count' => count( $plugins ),
|
||||
] );
|
||||
} else {
|
||||
// 同步所有供应商
|
||||
$all_plugins = $vendor_manager->get_all_plugins( true );
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => sprintf(
|
||||
wp_send_json_success( [
|
||||
'message' => sprintf(
|
||||
/* translators: %d: plugin count */
|
||||
__( '已同步 %d 个插件', 'wpbridge' ),
|
||||
count( $all_plugins )
|
||||
),
|
||||
'count' => count( $all_plugins ),
|
||||
)
|
||||
);
|
||||
__( '已同步 %d 个插件', 'wpbridge' ),
|
||||
count( $all_plugins )
|
||||
),
|
||||
'count' => count( $all_plugins ),
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -300,7 +291,7 @@ class VendorAdmin {
|
|||
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$plugin_slug = sanitize_key( $_POST['plugin_slug'] ?? '' );
|
||||
|
|
@ -308,13 +299,13 @@ class VendorAdmin {
|
|||
$update_url = esc_url_raw( $_POST['update_url'] ?? '' );
|
||||
|
||||
if ( empty( $plugin_slug ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '插件 slug 不能为空', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '插件 slug 不能为空', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$info = array(
|
||||
$info = [
|
||||
'name' => $name ?: $plugin_slug,
|
||||
'update_url' => $update_url,
|
||||
);
|
||||
];
|
||||
|
||||
$result = $this->get_bridge_manager()->add_custom_plugin( $plugin_slug, $info );
|
||||
|
||||
|
|
@ -334,13 +325,13 @@ class VendorAdmin {
|
|||
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$plugin_slug = sanitize_key( $_POST['plugin_slug'] ?? '' );
|
||||
|
||||
if ( empty( $plugin_slug ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '插件 slug 不能为空', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '插件 slug 不能为空', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$result = $this->get_bridge_manager()->remove_custom_plugin( $plugin_slug );
|
||||
|
|
@ -361,19 +352,19 @@ class VendorAdmin {
|
|||
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
$bridge_client = $this->get_bridge_manager()->get_bridge_client();
|
||||
|
||||
if ( ! $bridge_client ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Bridge Server 未配置', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( 'Bridge Server 未配置', 'wpbridge' ) ] );
|
||||
}
|
||||
|
||||
if ( $bridge_client->health_check() ) {
|
||||
wp_send_json_success( array( 'message' => __( '连接成功', 'wpbridge' ) ) );
|
||||
wp_send_json_success( [ 'message' => __( '连接成功', 'wpbridge' ) ] );
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( '连接失败', 'wpbridge' ) ) );
|
||||
wp_send_json_error( [ 'message' => __( '连接失败', 'wpbridge' ) ] );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -383,10 +374,10 @@ class VendorAdmin {
|
|||
* @return void
|
||||
*/
|
||||
public function render_vendor_settings(): void {
|
||||
$vendors = $this->get_bridge_manager()->get_vendors();
|
||||
$custom = $this->settings->get( 'custom_plugins', array() );
|
||||
$all_plugins = $this->get_bridge_manager()->get_all_available_plugins();
|
||||
$stats = $this->get_bridge_manager()->get_stats();
|
||||
$vendors = $this->get_bridge_manager()->get_vendors();
|
||||
$custom = $this->settings->get( 'custom_plugins', [] );
|
||||
$all_plugins = $this->get_bridge_manager()->get_all_available_plugins();
|
||||
$stats = $this->get_bridge_manager()->get_stats();
|
||||
|
||||
include WPBRIDGE_PATH . 'templates/admin/vendor-settings.php';
|
||||
}
|
||||
|
|
@ -397,14 +388,14 @@ class VendorAdmin {
|
|||
* @return array
|
||||
*/
|
||||
public function get_vendor_data(): array {
|
||||
return array(
|
||||
'vendors' => $this->get_bridge_manager()->get_vendors(),
|
||||
'custom' => $this->settings->get( 'custom_plugins', array() ),
|
||||
'all_plugins' => $this->get_bridge_manager()->get_all_available_plugins(),
|
||||
'stats' => $this->get_bridge_manager()->get_stats(),
|
||||
'vendor_types' => array(
|
||||
return [
|
||||
'vendors' => $this->get_bridge_manager()->get_vendors(),
|
||||
'custom' => $this->settings->get( 'custom_plugins', [] ),
|
||||
'all_plugins' => $this->get_bridge_manager()->get_all_available_plugins(),
|
||||
'stats' => $this->get_bridge_manager()->get_stats(),
|
||||
'vendor_types' => [
|
||||
'woocommerce' => __( 'WooCommerce 商店', 'wpbridge' ),
|
||||
),
|
||||
);
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,7 @@ namespace WPBridge\Cache;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,182 +17,182 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class CacheManager {
|
||||
|
||||
/**
|
||||
* 缓存组名
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_GROUP = 'wpbridge';
|
||||
/**
|
||||
* 缓存组名
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_GROUP = 'wpbridge';
|
||||
|
||||
/**
|
||||
* 默认 TTL(12 小时)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEFAULT_TTL = 43200;
|
||||
/**
|
||||
* 默认 TTL(12 小时)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEFAULT_TTL = 43200;
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*
|
||||
* @param string $key 缓存键
|
||||
* @return mixed|false
|
||||
*/
|
||||
public function get( string $key ) {
|
||||
// 优先使用对象缓存
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
$value = wp_cache_get( $key, self::CACHE_GROUP );
|
||||
if ( false !== $value ) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取缓存
|
||||
*
|
||||
* @param string $key 缓存键
|
||||
* @return mixed|false
|
||||
*/
|
||||
public function get( string $key ) {
|
||||
// 优先使用对象缓存
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
$value = wp_cache_get( $key, self::CACHE_GROUP );
|
||||
if ( false !== $value ) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
// 降级到 transient
|
||||
return get_transient( 'wpbridge_' . $key );
|
||||
}
|
||||
// 降级到 transient
|
||||
return get_transient( 'wpbridge_' . $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
*
|
||||
* @param string $key 缓存键
|
||||
* @param mixed $value 缓存值
|
||||
* @param int $ttl 过期时间(秒)
|
||||
* @return bool
|
||||
*/
|
||||
public function set( string $key, $value, int $ttl = self::DEFAULT_TTL ): bool {
|
||||
// 使用对象缓存
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
wp_cache_set( $key, $value, self::CACHE_GROUP, $ttl );
|
||||
}
|
||||
/**
|
||||
* 设置缓存
|
||||
*
|
||||
* @param string $key 缓存键
|
||||
* @param mixed $value 缓存值
|
||||
* @param int $ttl 过期时间(秒)
|
||||
* @return bool
|
||||
*/
|
||||
public function set( string $key, $value, int $ttl = self::DEFAULT_TTL ): bool {
|
||||
// 使用对象缓存
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
wp_cache_set( $key, $value, self::CACHE_GROUP, $ttl );
|
||||
}
|
||||
|
||||
// 同时存储到 transient
|
||||
return set_transient( 'wpbridge_' . $key, $value, $ttl );
|
||||
}
|
||||
// 同时存储到 transient
|
||||
return set_transient( 'wpbridge_' . $key, $value, $ttl );
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*
|
||||
* @param string $key 缓存键
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $key ): bool {
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
wp_cache_delete( $key, self::CACHE_GROUP );
|
||||
}
|
||||
/**
|
||||
* 删除缓存
|
||||
*
|
||||
* @param string $key 缓存键
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $key ): bool {
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
wp_cache_delete( $key, self::CACHE_GROUP );
|
||||
}
|
||||
|
||||
return delete_transient( 'wpbridge_' . $key );
|
||||
}
|
||||
return delete_transient( 'wpbridge_' . $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带过期缓存兜底的值
|
||||
*
|
||||
* @param string $key 缓存键
|
||||
* @param callable $callback 获取新值的回调
|
||||
* @param int $ttl 正常 TTL
|
||||
* @param int $stale_ttl 过期缓存可用时间
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_with_fallback( string $key, callable $callback, int $ttl = self::DEFAULT_TTL, int $stale_ttl = 604800 ) {
|
||||
// 尝试获取正常缓存
|
||||
$value = $this->get( $key );
|
||||
/**
|
||||
* 获取带过期缓存兜底的值
|
||||
*
|
||||
* @param string $key 缓存键
|
||||
* @param callable $callback 获取新值的回调
|
||||
* @param int $ttl 正常 TTL
|
||||
* @param int $stale_ttl 过期缓存可用时间
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_with_fallback( string $key, callable $callback, int $ttl = self::DEFAULT_TTL, int $stale_ttl = 604800 ) {
|
||||
// 尝试获取正常缓存
|
||||
$value = $this->get( $key );
|
||||
|
||||
if ( false !== $value ) {
|
||||
return $value;
|
||||
}
|
||||
if ( false !== $value ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// 尝试获取新值
|
||||
try {
|
||||
$new_value = $callback();
|
||||
// 尝试获取新值
|
||||
try {
|
||||
$new_value = $callback();
|
||||
|
||||
if ( null !== $new_value && false !== $new_value ) {
|
||||
$this->set( $key, $new_value, $ttl );
|
||||
if ( null !== $new_value && false !== $new_value ) {
|
||||
$this->set( $key, $new_value, $ttl );
|
||||
|
||||
// 同时存储一份过期缓存备份
|
||||
$this->set( $key . '_stale', $new_value, $stale_ttl );
|
||||
// 同时存储一份过期缓存备份
|
||||
$this->set( $key . '_stale', $new_value, $stale_ttl );
|
||||
|
||||
return $new_value;
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
// 获取新值失败,尝试使用过期缓存
|
||||
}
|
||||
return $new_value;
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
// 获取新值失败,尝试使用过期缓存
|
||||
}
|
||||
|
||||
// 尝试使用过期缓存
|
||||
$stale_value = $this->get( $key . '_stale' );
|
||||
// 尝试使用过期缓存
|
||||
$stale_value = $this->get( $key . '_stale' );
|
||||
|
||||
if ( false !== $stale_value ) {
|
||||
return $stale_value;
|
||||
}
|
||||
if ( false !== $stale_value ) {
|
||||
return $stale_value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*/
|
||||
public function flush(): void {
|
||||
global $wpdb;
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*/
|
||||
public function flush(): void {
|
||||
global $wpdb;
|
||||
|
||||
// 清除 transient(使用 prepare 防止 SQL 注入)
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_wpbridge_' ) . '%'
|
||||
)
|
||||
);
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_timeout_wpbridge_' ) . '%'
|
||||
)
|
||||
);
|
||||
// 清除 transient(使用 prepare 防止 SQL 注入)
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_wpbridge_' ) . '%'
|
||||
)
|
||||
);
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_timeout_wpbridge_' ) . '%'
|
||||
)
|
||||
);
|
||||
|
||||
// 清除对象缓存组(不使用 flush 避免影响其他插件)
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
wp_cache_delete( 'wpbridge', 'wpbridge' );
|
||||
}
|
||||
}
|
||||
// 清除对象缓存组(不使用 flush 避免影响其他插件)
|
||||
if ( wp_using_ext_object_cache() ) {
|
||||
wp_cache_delete( 'wpbridge', 'wpbridge' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
global $wpdb;
|
||||
/**
|
||||
* 获取缓存统计
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
global $wpdb;
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_wpbridge_' ) . '%'
|
||||
)
|
||||
);
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_wpbridge_' ) . '%'
|
||||
)
|
||||
);
|
||||
|
||||
return array(
|
||||
'transient_count' => (int) $count,
|
||||
'object_cache' => wp_using_ext_object_cache(),
|
||||
'object_cache_type' => $this->get_object_cache_type(),
|
||||
);
|
||||
}
|
||||
return [
|
||||
'transient_count' => (int) $count,
|
||||
'object_cache' => wp_using_ext_object_cache(),
|
||||
'object_cache_type' => $this->get_object_cache_type(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象缓存类型
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_object_cache_type(): string {
|
||||
if ( ! wp_using_ext_object_cache() ) {
|
||||
return 'none';
|
||||
}
|
||||
/**
|
||||
* 获取对象缓存类型
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_object_cache_type(): string {
|
||||
if ( ! wp_using_ext_object_cache() ) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
global $wp_object_cache;
|
||||
global $wp_object_cache;
|
||||
|
||||
if ( isset( $wp_object_cache->redis ) || class_exists( 'Redis' ) ) {
|
||||
return 'redis';
|
||||
}
|
||||
if ( isset( $wp_object_cache->redis ) || class_exists( 'Redis' ) ) {
|
||||
return 'redis';
|
||||
}
|
||||
|
||||
if ( isset( $wp_object_cache->mc ) || class_exists( 'Memcached' ) ) {
|
||||
return 'memcached';
|
||||
}
|
||||
if ( isset( $wp_object_cache->mc ) || class_exists( 'Memcached' ) ) {
|
||||
return 'memcached';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,230 +22,222 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class FallbackStrategy {
|
||||
|
||||
/**
|
||||
* 源不可用时的行为
|
||||
*/
|
||||
const ON_FAIL_SKIP = 'skip'; // 跳过,继续下一个源
|
||||
const ON_FAIL_WARN = 'warn'; // 警告,但继续
|
||||
const ON_FAIL_BLOCK = 'block'; // 阻止更新检查
|
||||
/**
|
||||
* 源不可用时的行为
|
||||
*/
|
||||
const ON_FAIL_SKIP = 'skip'; // 跳过,继续下一个源
|
||||
const ON_FAIL_WARN = 'warn'; // 警告,但继续
|
||||
const ON_FAIL_BLOCK = 'block'; // 阻止更新检查
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 缓存管理器
|
||||
*
|
||||
* @var CacheManager
|
||||
*/
|
||||
private CacheManager $cache;
|
||||
/**
|
||||
* 缓存管理器
|
||||
*
|
||||
* @var CacheManager
|
||||
*/
|
||||
private CacheManager $cache;
|
||||
|
||||
/**
|
||||
* 健康检查器
|
||||
*
|
||||
* @var HealthChecker
|
||||
*/
|
||||
private HealthChecker $health_checker;
|
||||
/**
|
||||
* 健康检查器
|
||||
*
|
||||
* @var HealthChecker
|
||||
*/
|
||||
private HealthChecker $health_checker;
|
||||
|
||||
/**
|
||||
* 最大重试次数
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAX_RETRIES = 2;
|
||||
/**
|
||||
* 最大重试次数
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAX_RETRIES = 2;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->cache = new CacheManager();
|
||||
$this->health_checker = new HealthChecker();
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->cache = new CacheManager();
|
||||
$this->health_checker = new HealthChecker();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的源列表(排除不可用的)
|
||||
*
|
||||
* @param SourceModel[] $sources 源列表
|
||||
* @return SourceModel[]
|
||||
*/
|
||||
public function get_available_sources( array $sources ): array {
|
||||
$available = array();
|
||||
/**
|
||||
* 获取可用的源列表(排除不可用的)
|
||||
*
|
||||
* @param SourceModel[] $sources 源列表
|
||||
* @return SourceModel[]
|
||||
*/
|
||||
public function get_available_sources( array $sources ): array {
|
||||
$available = [];
|
||||
|
||||
foreach ( $sources as $source ) {
|
||||
// 检查是否在冷却期
|
||||
if ( $this->health_checker->is_in_cooldown( $source->id ) ) {
|
||||
Logger::debug( '源在冷却期,跳过', array( 'source' => $source->id ) );
|
||||
continue;
|
||||
}
|
||||
foreach ( $sources as $source ) {
|
||||
// 检查是否在冷却期
|
||||
if ( $this->health_checker->is_in_cooldown( $source->id ) ) {
|
||||
Logger::debug( '源在冷却期,跳过', [ 'source' => $source->id ] );
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查缓存的健康状态
|
||||
$status = $this->health_checker->get_status( $source->id );
|
||||
// 检查缓存的健康状态
|
||||
$status = $this->health_checker->get_status( $source->id );
|
||||
|
||||
if ( null !== $status && ! $status->is_available() ) {
|
||||
Logger::debug( '源不可用,跳过', array( 'source' => $source->id ) );
|
||||
continue;
|
||||
}
|
||||
if ( null !== $status && ! $status->is_available() ) {
|
||||
Logger::debug( '源不可用,跳过', [ 'source' => $source->id ] );
|
||||
continue;
|
||||
}
|
||||
|
||||
$available[] = $source;
|
||||
}
|
||||
$available[] = $source;
|
||||
}
|
||||
|
||||
return $available;
|
||||
}
|
||||
return $available;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行带降级的操作
|
||||
*
|
||||
* @param SourceModel[] $sources 源列表
|
||||
* @param callable $callback 操作回调,接收 SourceModel 参数
|
||||
* @param string $cache_key 缓存键(用于过期缓存兜底)
|
||||
* @return mixed
|
||||
*/
|
||||
public function execute_with_fallback( array $sources, callable $callback, string $cache_key = '' ) {
|
||||
$available = $this->get_available_sources( $sources );
|
||||
/**
|
||||
* 执行带降级的操作
|
||||
*
|
||||
* @param SourceModel[] $sources 源列表
|
||||
* @param callable $callback 操作回调,接收 SourceModel 参数
|
||||
* @param string $cache_key 缓存键(用于过期缓存兜底)
|
||||
* @return mixed
|
||||
*/
|
||||
public function execute_with_fallback( array $sources, callable $callback, string $cache_key = '' ) {
|
||||
$available = $this->get_available_sources( $sources );
|
||||
|
||||
if ( empty( $available ) ) {
|
||||
Logger::warning( '没有可用的更新源' );
|
||||
if ( empty( $available ) ) {
|
||||
Logger::warning( '没有可用的更新源' );
|
||||
|
||||
// 尝试使用过期缓存
|
||||
if ( ! empty( $cache_key ) ) {
|
||||
$stale = $this->cache->get( $cache_key . '_stale' );
|
||||
if ( false !== $stale ) {
|
||||
Logger::info( '使用过期缓存', array( 'key' => $cache_key ) );
|
||||
return $stale;
|
||||
}
|
||||
}
|
||||
// 尝试使用过期缓存
|
||||
if ( ! empty( $cache_key ) ) {
|
||||
$stale = $this->cache->get( $cache_key . '_stale' );
|
||||
if ( false !== $stale ) {
|
||||
Logger::info( '使用过期缓存', [ 'key' => $cache_key ] );
|
||||
return $stale;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$last_error = null;
|
||||
$last_error = null;
|
||||
|
||||
foreach ( $available as $source ) {
|
||||
$retries = 0;
|
||||
foreach ( $available as $source ) {
|
||||
$retries = 0;
|
||||
|
||||
while ( $retries < self::MAX_RETRIES ) {
|
||||
try {
|
||||
$result = $callback( $source );
|
||||
while ( $retries < self::MAX_RETRIES ) {
|
||||
try {
|
||||
$result = $callback( $source );
|
||||
|
||||
if ( null !== $result && false !== $result ) {
|
||||
// 成功,缓存结果
|
||||
if ( ! empty( $cache_key ) ) {
|
||||
$this->cache->set( $cache_key, $result );
|
||||
$this->cache->set( $cache_key . '_stale', $result, 604800 ); // 7 天
|
||||
}
|
||||
if ( null !== $result && false !== $result ) {
|
||||
// 成功,缓存结果
|
||||
if ( ! empty( $cache_key ) ) {
|
||||
$this->cache->set( $cache_key, $result );
|
||||
$this->cache->set( $cache_key . '_stale', $result, 604800 ); // 7 天
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 返回 null/false 但没有异常,不重试
|
||||
break;
|
||||
// 返回 null/false 但没有异常,不重试
|
||||
break;
|
||||
|
||||
} catch ( \Exception $e ) {
|
||||
$last_error = $e;
|
||||
++$retries;
|
||||
} catch ( \Exception $e ) {
|
||||
$last_error = $e;
|
||||
$retries++;
|
||||
|
||||
Logger::warning(
|
||||
'操作失败,重试中',
|
||||
array(
|
||||
'source' => $source->id,
|
||||
'retry' => $retries,
|
||||
'error' => $e->getMessage(),
|
||||
)
|
||||
);
|
||||
Logger::warning( '操作失败,重试中', [
|
||||
'source' => $source->id,
|
||||
'retry' => $retries,
|
||||
'error' => $e->getMessage(),
|
||||
] );
|
||||
|
||||
if ( $retries >= self::MAX_RETRIES ) {
|
||||
// 标记源为失败
|
||||
$this->health_checker->check( $source, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $retries >= self::MAX_RETRIES ) {
|
||||
// 标记源为失败
|
||||
$this->health_checker->check( $source, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有源都失败,尝试使用过期缓存
|
||||
if ( ! empty( $cache_key ) ) {
|
||||
$stale = $this->cache->get( $cache_key . '_stale' );
|
||||
if ( false !== $stale ) {
|
||||
Logger::info( '所有源失败,使用过期缓存', array( 'key' => $cache_key ) );
|
||||
return $stale;
|
||||
}
|
||||
}
|
||||
// 所有源都失败,尝试使用过期缓存
|
||||
if ( ! empty( $cache_key ) ) {
|
||||
$stale = $this->cache->get( $cache_key . '_stale' );
|
||||
if ( false !== $stale ) {
|
||||
Logger::info( '所有源失败,使用过期缓存', [ 'key' => $cache_key ] );
|
||||
return $stale;
|
||||
}
|
||||
}
|
||||
|
||||
if ( null !== $last_error ) {
|
||||
Logger::error( '所有源都失败', array( 'error' => $last_error->getMessage() ) );
|
||||
}
|
||||
if ( null !== $last_error ) {
|
||||
Logger::error( '所有源都失败', [ 'error' => $last_error->getMessage() ] );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理源失败
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
* @param string $error 错误信息
|
||||
*/
|
||||
public function handle_source_failure( SourceModel $source, string $error ): void {
|
||||
$behavior = $this->settings->get( 'on_source_fail', self::ON_FAIL_SKIP );
|
||||
/**
|
||||
* 处理源失败
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
* @param string $error 错误信息
|
||||
*/
|
||||
public function handle_source_failure( SourceModel $source, string $error ): void {
|
||||
$behavior = $this->settings->get( 'on_source_fail', self::ON_FAIL_SKIP );
|
||||
|
||||
Logger::warning(
|
||||
'源失败',
|
||||
array(
|
||||
'source' => $source->id,
|
||||
'error' => $error,
|
||||
'behavior' => $behavior,
|
||||
)
|
||||
);
|
||||
Logger::warning( '源失败', [
|
||||
'source' => $source->id,
|
||||
'error' => $error,
|
||||
'behavior' => $behavior,
|
||||
] );
|
||||
|
||||
switch ( $behavior ) {
|
||||
case self::ON_FAIL_WARN:
|
||||
// 添加管理员通知
|
||||
$this->add_admin_notice( $source, $error );
|
||||
break;
|
||||
switch ( $behavior ) {
|
||||
case self::ON_FAIL_WARN:
|
||||
// 添加管理员通知
|
||||
$this->add_admin_notice( $source, $error );
|
||||
break;
|
||||
|
||||
case self::ON_FAIL_BLOCK:
|
||||
// 阻止更新检查(不推荐)
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
__( '更新源 %1$s 不可用: %2$s', 'wpbridge' ),
|
||||
$source->name,
|
||||
$error
|
||||
)
|
||||
);
|
||||
case self::ON_FAIL_BLOCK:
|
||||
// 阻止更新检查(不推荐)
|
||||
throw new \RuntimeException( sprintf(
|
||||
__( '更新源 %s 不可用: %s', 'wpbridge' ),
|
||||
$source->name,
|
||||
$error
|
||||
) );
|
||||
|
||||
case self::ON_FAIL_SKIP:
|
||||
default:
|
||||
// 静默跳过
|
||||
break;
|
||||
}
|
||||
}
|
||||
case self::ON_FAIL_SKIP:
|
||||
default:
|
||||
// 静默跳过
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加管理员通知
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
* @param string $error 错误信息
|
||||
*/
|
||||
private function add_admin_notice( SourceModel $source, string $error ): void {
|
||||
$notices = get_option( 'wpbridge_admin_notices', array() );
|
||||
/**
|
||||
* 添加管理员通知
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
* @param string $error 错误信息
|
||||
*/
|
||||
private function add_admin_notice( SourceModel $source, string $error ): void {
|
||||
$notices = get_option( 'wpbridge_admin_notices', [] );
|
||||
|
||||
$notices[] = array(
|
||||
'type' => 'warning',
|
||||
'message' => sprintf(
|
||||
__( '更新源 "%1$s" 暂时不可用: %2$s', 'wpbridge' ),
|
||||
$source->name,
|
||||
$error
|
||||
),
|
||||
'time' => time(),
|
||||
);
|
||||
$notices[] = [
|
||||
'type' => 'warning',
|
||||
'message' => sprintf(
|
||||
__( '更新源 "%s" 暂时不可用: %s', 'wpbridge' ),
|
||||
$source->name,
|
||||
$error
|
||||
),
|
||||
'time' => time(),
|
||||
];
|
||||
|
||||
// 只保留最近 10 条通知
|
||||
$notices = array_slice( $notices, -10 );
|
||||
// 只保留最近 10 条通知
|
||||
$notices = array_slice( $notices, -10 );
|
||||
|
||||
update_option( 'wpbridge_admin_notices', $notices );
|
||||
}
|
||||
update_option( 'wpbridge_admin_notices', $notices );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -21,173 +21,167 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class HealthChecker {
|
||||
|
||||
/**
|
||||
* 缓存管理器
|
||||
*
|
||||
* @var CacheManager
|
||||
*/
|
||||
private CacheManager $cache;
|
||||
/**
|
||||
* 缓存管理器
|
||||
*
|
||||
* @var CacheManager
|
||||
*/
|
||||
private CacheManager $cache;
|
||||
|
||||
/**
|
||||
* 健康状态缓存 TTL(1 小时)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const HEALTH_CACHE_TTL = 3600;
|
||||
/**
|
||||
* 健康状态缓存 TTL(1 小时)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const HEALTH_CACHE_TTL = 3600;
|
||||
|
||||
/**
|
||||
* 失败源冷却时间(30 分钟)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const FAILED_COOLDOWN = 1800;
|
||||
/**
|
||||
* 失败源冷却时间(30 分钟)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const FAILED_COOLDOWN = 1800;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->cache = new CacheManager();
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->cache = new CacheManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查源健康状态
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
* @param bool $force 是否强制检查
|
||||
* @return HealthStatus
|
||||
*/
|
||||
public function check( SourceModel $source, bool $force = false ): HealthStatus {
|
||||
$cache_key = 'health_' . $source->id;
|
||||
/**
|
||||
* 检查源健康状态
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
* @param bool $force 是否强制检查
|
||||
* @return HealthStatus
|
||||
*/
|
||||
public function check( SourceModel $source, bool $force = false ): HealthStatus {
|
||||
$cache_key = 'health_' . $source->id;
|
||||
|
||||
// 检查缓存
|
||||
if ( ! $force ) {
|
||||
$cached = $this->cache->get( $cache_key );
|
||||
if ( false !== $cached && $cached instanceof HealthStatus ) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
// 检查缓存
|
||||
if ( ! $force ) {
|
||||
$cached = $this->cache->get( $cache_key );
|
||||
if ( false !== $cached && $cached instanceof HealthStatus ) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否在冷却期
|
||||
if ( ! $force && $this->is_in_cooldown( $source->id ) ) {
|
||||
return HealthStatus::failed( __( '源在冷却期内', 'wpbridge' ) );
|
||||
}
|
||||
// 检查是否在冷却期
|
||||
if ( ! $force && $this->is_in_cooldown( $source->id ) ) {
|
||||
return HealthStatus::failed( __( '源在冷却期内', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
// 执行健康检查
|
||||
$handler = $source->get_handler();
|
||||
// 执行健康检查
|
||||
$handler = $source->get_handler();
|
||||
|
||||
if ( null === $handler ) {
|
||||
$status = HealthStatus::failed( __( '无法获取处理器', 'wpbridge' ) );
|
||||
} else {
|
||||
$status = $handler->test_connection();
|
||||
}
|
||||
if ( null === $handler ) {
|
||||
$status = HealthStatus::failed( __( '无法获取处理器', 'wpbridge' ) );
|
||||
} else {
|
||||
$status = $handler->test_connection();
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
$ttl = $status->is_healthy() ? self::HEALTH_CACHE_TTL : self::FAILED_COOLDOWN;
|
||||
$this->cache->set( $cache_key, $status, $ttl );
|
||||
// 缓存结果
|
||||
$ttl = $status->is_healthy() ? self::HEALTH_CACHE_TTL : self::FAILED_COOLDOWN;
|
||||
$this->cache->set( $cache_key, $status, $ttl );
|
||||
|
||||
// 如果失败,设置冷却
|
||||
if ( ! $status->is_available() ) {
|
||||
$this->set_cooldown( $source->id );
|
||||
}
|
||||
// 如果失败,设置冷却
|
||||
if ( ! $status->is_available() ) {
|
||||
$this->set_cooldown( $source->id );
|
||||
}
|
||||
|
||||
Logger::debug(
|
||||
'健康检查完成',
|
||||
array(
|
||||
'source' => $source->id,
|
||||
'status' => $status->status,
|
||||
'time' => $status->response_time,
|
||||
)
|
||||
);
|
||||
Logger::debug( '健康检查完成', [
|
||||
'source' => $source->id,
|
||||
'status' => $status->status,
|
||||
'time' => $status->response_time,
|
||||
] );
|
||||
|
||||
return $status;
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量检查源健康状态
|
||||
*
|
||||
* @param SourceModel[] $sources 源列表
|
||||
* @return array<string, HealthStatus>
|
||||
*/
|
||||
public function check_all( array $sources ): array {
|
||||
$results = array();
|
||||
/**
|
||||
* 批量检查源健康状态
|
||||
*
|
||||
* @param SourceModel[] $sources 源列表
|
||||
* @return array<string, HealthStatus>
|
||||
*/
|
||||
public function check_all( array $sources ): array {
|
||||
$results = [];
|
||||
|
||||
foreach ( $sources as $source ) {
|
||||
$results[ $source->id ] = $this->check( $source );
|
||||
}
|
||||
foreach ( $sources as $source ) {
|
||||
$results[ $source->id ] = $this->check( $source );
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取源健康状态(仅从缓存)
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return HealthStatus|null
|
||||
*/
|
||||
public function get_status( string $source_id ): ?HealthStatus {
|
||||
$cached = $this->cache->get( 'health_' . $source_id );
|
||||
/**
|
||||
* 获取源健康状态(仅从缓存)
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return HealthStatus|null
|
||||
*/
|
||||
public function get_status( string $source_id ): ?HealthStatus {
|
||||
$cached = $this->cache->get( 'health_' . $source_id );
|
||||
|
||||
if ( false !== $cached && $cached instanceof HealthStatus ) {
|
||||
return $cached;
|
||||
}
|
||||
if ( false !== $cached && $cached instanceof HealthStatus ) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查源是否在冷却期
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function is_in_cooldown( string $source_id ): bool {
|
||||
$cooldown = $this->cache->get( 'cooldown_' . $source_id );
|
||||
return false !== $cooldown;
|
||||
}
|
||||
/**
|
||||
* 检查源是否在冷却期
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function is_in_cooldown( string $source_id ): bool {
|
||||
$cooldown = $this->cache->get( 'cooldown_' . $source_id );
|
||||
return false !== $cooldown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置源冷却
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
private function set_cooldown( string $source_id ): void {
|
||||
$this->cache->set( 'cooldown_' . $source_id, time(), self::FAILED_COOLDOWN );
|
||||
/**
|
||||
* 设置源冷却
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
private function set_cooldown( string $source_id ): void {
|
||||
$this->cache->set( 'cooldown_' . $source_id, time(), self::FAILED_COOLDOWN );
|
||||
|
||||
Logger::info(
|
||||
'源进入冷却期',
|
||||
array(
|
||||
'source' => $source_id,
|
||||
'duration' => self::FAILED_COOLDOWN,
|
||||
)
|
||||
);
|
||||
}
|
||||
Logger::info( '源进入冷却期', [
|
||||
'source' => $source_id,
|
||||
'duration' => self::FAILED_COOLDOWN,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除源冷却
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
public function clear_cooldown( string $source_id ): void {
|
||||
$this->cache->delete( 'cooldown_' . $source_id );
|
||||
}
|
||||
/**
|
||||
* 清除源冷却
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
public function clear_cooldown( string $source_id ): void {
|
||||
$this->cache->delete( 'cooldown_' . $source_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有健康状态缓存
|
||||
*/
|
||||
public function clear_all(): void {
|
||||
global $wpdb;
|
||||
/**
|
||||
* 清除所有健康状态缓存
|
||||
*/
|
||||
public function clear_all(): void {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_wpbridge_health_' ) . '%'
|
||||
)
|
||||
);
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_wpbridge_cooldown_' ) . '%'
|
||||
)
|
||||
);
|
||||
}
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_wpbridge_health_' ) . '%'
|
||||
)
|
||||
);
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_wpbridge_cooldown_' ) . '%'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,13 +68,10 @@ class BridgeClient {
|
|||
$response = $this->request( 'GET', "/api/v1/plugin/{$slug}" );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error(
|
||||
'Failed to get plugin info',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'error' => $response->get_error_message(),
|
||||
)
|
||||
);
|
||||
Logger::error( 'Failed to get plugin info', [
|
||||
'slug' => $slug,
|
||||
'error' => $response->get_error_message(),
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -98,19 +95,16 @@ class BridgeClient {
|
|||
* @return array
|
||||
*/
|
||||
public function list_vendors(): array {
|
||||
$response = $this->request( 'GET', '/api/v1/admin/vendors', array(), true );
|
||||
$response = $this->request( 'GET', '/api/v1/admin/vendors', [], true );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error(
|
||||
'Failed to list vendors',
|
||||
array(
|
||||
'error' => $response->get_error_message(),
|
||||
)
|
||||
);
|
||||
return array();
|
||||
Logger::error( 'Failed to list vendors', [
|
||||
'error' => $response->get_error_message(),
|
||||
] );
|
||||
return [];
|
||||
}
|
||||
|
||||
return $response ?? array();
|
||||
return $response ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -123,16 +117,16 @@ class BridgeClient {
|
|||
$response = $this->request( 'POST', '/api/v1/admin/vendors', $data, true );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $response->get_error_message(),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $response,
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -146,16 +140,16 @@ class BridgeClient {
|
|||
$response = $this->request( 'PUT', "/api/v1/admin/vendors/{$id}", $data, true );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $response->get_error_message(),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $response,
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -165,18 +159,18 @@ class BridgeClient {
|
|||
* @return array
|
||||
*/
|
||||
public function delete_vendor( int $id ): array {
|
||||
$response = $this->request( 'DELETE', "/api/v1/admin/vendors/{$id}", array(), true );
|
||||
$response = $this->request( 'DELETE', "/api/v1/admin/vendors/{$id}", [], true );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $response->get_error_message(),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -185,19 +179,16 @@ class BridgeClient {
|
|||
* @return array
|
||||
*/
|
||||
public function list_plugins(): array {
|
||||
$response = $this->request( 'GET', '/api/v1/admin/plugins', array(), true );
|
||||
$response = $this->request( 'GET', '/api/v1/admin/plugins', [], true );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error(
|
||||
'Failed to list plugins',
|
||||
array(
|
||||
'error' => $response->get_error_message(),
|
||||
)
|
||||
);
|
||||
return array();
|
||||
Logger::error( 'Failed to list plugins', [
|
||||
'error' => $response->get_error_message(),
|
||||
] );
|
||||
return [];
|
||||
}
|
||||
|
||||
return $response ?? array();
|
||||
return $response ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -210,16 +201,16 @@ class BridgeClient {
|
|||
$response = $this->request( 'POST', '/api/v1/admin/plugins', $data, true );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $response->get_error_message(),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $response,
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -229,7 +220,7 @@ class BridgeClient {
|
|||
* @return array|null
|
||||
*/
|
||||
public function get_plugin( string $slug ): ?array {
|
||||
$response = $this->request( 'GET', "/api/v1/admin/plugins/{$slug}", array(), true );
|
||||
$response = $this->request( 'GET', "/api/v1/admin/plugins/{$slug}", [], true );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return null;
|
||||
|
|
@ -249,16 +240,16 @@ class BridgeClient {
|
|||
$response = $this->request( 'PUT', "/api/v1/admin/plugins/{$slug}", $data, true );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $response->get_error_message(),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $response,
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -268,18 +259,18 @@ class BridgeClient {
|
|||
* @return array
|
||||
*/
|
||||
public function delete_plugin( string $slug ): array {
|
||||
$response = $this->request( 'DELETE', "/api/v1/admin/plugins/{$slug}", array(), true );
|
||||
$response = $this->request( 'DELETE', "/api/v1/admin/plugins/{$slug}", [], true );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $response->get_error_message(),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -306,17 +297,17 @@ class BridgeClient {
|
|||
* @param bool $auth 是否需要认证
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
private function request( string $method, string $endpoint, array $data = array(), bool $auth = false ) {
|
||||
private function request( string $method, string $endpoint, array $data = [], bool $auth = false ) {
|
||||
$url = $this->server_url . $endpoint;
|
||||
|
||||
$args = array(
|
||||
$args = [
|
||||
'method' => $method,
|
||||
'timeout' => $this->timeout,
|
||||
'headers' => array(
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
),
|
||||
);
|
||||
],
|
||||
];
|
||||
|
||||
// 添加认证头
|
||||
if ( $auth && ! empty( $this->api_key ) ) {
|
||||
|
|
@ -324,7 +315,7 @@ class BridgeClient {
|
|||
}
|
||||
|
||||
// 添加请求体
|
||||
if ( ! empty( $data ) && in_array( $method, array( 'POST', 'PUT', 'PATCH' ), true ) ) {
|
||||
if ( ! empty( $data ) && in_array( $method, [ 'POST', 'PUT', 'PATCH' ], true ) ) {
|
||||
$args['body'] = wp_json_encode( $data );
|
||||
}
|
||||
|
||||
|
|
@ -339,14 +330,14 @@ class BridgeClient {
|
|||
|
||||
// 处理 204 No Content
|
||||
if ( $status_code === 204 ) {
|
||||
return array();
|
||||
return [];
|
||||
}
|
||||
|
||||
// 处理错误状态码
|
||||
if ( $status_code >= 400 ) {
|
||||
$error_data = json_decode( $body, true );
|
||||
$message = $error_data['message'] ?? $error_data['error'] ?? "HTTP {$status_code}";
|
||||
return new \WP_Error( 'bridge_server_error', $message, array( 'status' => $status_code ) );
|
||||
return new \WP_Error( 'bridge_server_error', $message, [ 'status' => $status_code ] );
|
||||
}
|
||||
|
||||
// 解析 JSON 响应
|
||||
|
|
|
|||
|
|
@ -118,10 +118,10 @@ class BridgeManager {
|
|||
$client = new BridgeClient( $server_url, $api_key );
|
||||
|
||||
if ( ! $client->health_check() ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __( '无法连接到 Bridge Server', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
// 保存配置(URL 明文存储,API Key 加密存储)
|
||||
|
|
@ -130,12 +130,12 @@ class BridgeManager {
|
|||
|
||||
$this->bridge_client = $client;
|
||||
|
||||
Logger::info( 'Bridge server configured', array( 'url' => $server_url ) );
|
||||
Logger::info( 'Bridge server configured', [ 'url' => $server_url ] );
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __( 'Bridge Server 配置成功', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -144,7 +144,7 @@ class BridgeManager {
|
|||
* @return void
|
||||
*/
|
||||
private function init_vendors(): void {
|
||||
$vendor_configs = $this->settings->get( 'vendors', array() );
|
||||
$vendor_configs = $this->settings->get( 'vendors', [] );
|
||||
|
||||
foreach ( $vendor_configs as $vendor_id => $config ) {
|
||||
if ( empty( $config['enabled'] ) ) {
|
||||
|
|
@ -182,7 +182,7 @@ class BridgeManager {
|
|||
$plugins = $this->bridge_client->list_plugins();
|
||||
if ( ! empty( $plugins ) ) {
|
||||
// 转换为 slug => info 格式
|
||||
$result = array();
|
||||
$result = [];
|
||||
foreach ( $plugins as $plugin ) {
|
||||
$result[ $plugin['slug'] ] = $plugin;
|
||||
}
|
||||
|
|
@ -191,7 +191,7 @@ class BridgeManager {
|
|||
}
|
||||
|
||||
// 回退到 RemoteConfig
|
||||
return $this->remote_config->get( 'bridgeable_plugins', array() );
|
||||
return $this->remote_config->get( 'bridgeable_plugins', [] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -219,18 +219,15 @@ class BridgeManager {
|
|||
* @return array
|
||||
*/
|
||||
public function get_all_available_plugins(): array {
|
||||
$plugins = array();
|
||||
$plugins = [];
|
||||
|
||||
// 1. 官方优化列表
|
||||
$official = $this->get_available_plugins();
|
||||
foreach ( $official as $slug => $info ) {
|
||||
$plugins[ $slug ] = array_merge(
|
||||
$info,
|
||||
array(
|
||||
'source' => 'official',
|
||||
'vendor' => null,
|
||||
)
|
||||
);
|
||||
$plugins[ $slug ] = array_merge( $info, [
|
||||
'source' => 'official',
|
||||
'vendor' => null,
|
||||
] );
|
||||
}
|
||||
|
||||
// 2. 供应商渠道插件
|
||||
|
|
@ -245,16 +242,13 @@ class BridgeManager {
|
|||
}
|
||||
|
||||
// 3. 用户自定义插件
|
||||
$custom = $this->settings->get( 'custom_plugins', array() );
|
||||
$custom = $this->settings->get( 'custom_plugins', [] );
|
||||
foreach ( $custom as $slug => $info ) {
|
||||
if ( ! isset( $plugins[ $slug ] ) ) {
|
||||
$plugins[ $slug ] = array_merge(
|
||||
$info,
|
||||
array(
|
||||
'source' => 'custom',
|
||||
'vendor' => null,
|
||||
)
|
||||
);
|
||||
$plugins[ $slug ] = array_merge( $info, [
|
||||
'source' => 'custom',
|
||||
'vendor' => null,
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -276,7 +270,7 @@ class BridgeManager {
|
|||
* @return array
|
||||
*/
|
||||
public function get_bridged_plugins(): array {
|
||||
return $this->settings->get( 'bridged_plugins', array() );
|
||||
return $this->settings->get( 'bridged_plugins', [] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -290,11 +284,11 @@ class BridgeManager {
|
|||
// 1. 检查是否在可桥接列表(混合模式)
|
||||
$all_available = $this->get_all_available_plugins();
|
||||
if ( ! isset( $all_available[ $plugin_slug ] ) ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __( '该插件不在可桥接列表中', 'wpbridge' ),
|
||||
'code' => 'not_available',
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
$plugin_info = $all_available[ $plugin_slug ];
|
||||
|
|
@ -302,40 +296,34 @@ class BridgeManager {
|
|||
// 2. H5 修复: GPL 合规验证
|
||||
$gpl_result = $this->gpl_validator->validate( $plugin_slug, $plugin_file );
|
||||
if ( $gpl_result['is_gpl'] === false ) {
|
||||
Logger::warning(
|
||||
'GPL validation failed',
|
||||
array(
|
||||
'plugin' => $plugin_slug,
|
||||
'result' => $gpl_result,
|
||||
)
|
||||
);
|
||||
return array(
|
||||
Logger::warning( 'GPL validation failed', [
|
||||
'plugin' => $plugin_slug,
|
||||
'result' => $gpl_result,
|
||||
] );
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __( '该插件不是 GPL 授权,无法桥接', 'wpbridge' ),
|
||||
'code' => 'not_gpl',
|
||||
'license' => $gpl_result['license'],
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
if ( $gpl_result['is_gpl'] === null && $gpl_result['confidence'] < 50 ) {
|
||||
// 无法确定,但置信度低,警告用户
|
||||
Logger::info(
|
||||
'GPL validation uncertain',
|
||||
array(
|
||||
'plugin' => $plugin_slug,
|
||||
'result' => $gpl_result,
|
||||
)
|
||||
);
|
||||
Logger::info( 'GPL validation uncertain', [
|
||||
'plugin' => $plugin_slug,
|
||||
'result' => $gpl_result,
|
||||
] );
|
||||
}
|
||||
|
||||
// 3. 检查订阅限制
|
||||
$limit_check = $this->check_subscription_limit();
|
||||
if ( ! $limit_check['allowed'] ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $limit_check['message'],
|
||||
'code' => 'limit_exceeded',
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
// 4. 添加到桥接列表
|
||||
|
|
@ -344,23 +332,20 @@ class BridgeManager {
|
|||
$bridged[] = $plugin_slug;
|
||||
$this->settings->set( 'bridged_plugins', $bridged );
|
||||
|
||||
Logger::info(
|
||||
'Plugin bridge enabled',
|
||||
array(
|
||||
'plugin' => $plugin_slug,
|
||||
'gpl_result' => $gpl_result,
|
||||
)
|
||||
);
|
||||
Logger::info( 'Plugin bridge enabled', [
|
||||
'plugin' => $plugin_slug,
|
||||
'gpl_result' => $gpl_result,
|
||||
] );
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'message' => __( '桥接已启用', 'wpbridge' ),
|
||||
'code' => 'enabled',
|
||||
'gpl_result' => $gpl_result,
|
||||
'source' => $plugin_info['source'] ?? 'official',
|
||||
'vendor' => $plugin_info['vendor'] ?? null,
|
||||
);
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __( '桥接已启用', 'wpbridge' ),
|
||||
'code' => 'enabled',
|
||||
'gpl_result' => $gpl_result,
|
||||
'source' => $plugin_info['source'] ?? 'official',
|
||||
'vendor' => $plugin_info['vendor'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -371,21 +356,21 @@ class BridgeManager {
|
|||
*/
|
||||
public function disable_bridge( string $plugin_slug ): array {
|
||||
$bridged = $this->get_bridged_plugins();
|
||||
$bridged = array_diff( $bridged, array( $plugin_slug ) );
|
||||
$bridged = array_diff( $bridged, [ $plugin_slug ] );
|
||||
$result = $this->settings->set( 'bridged_plugins', array_values( $bridged ) );
|
||||
|
||||
if ( $result ) {
|
||||
Logger::info( 'Plugin bridge disabled', array( 'plugin' => $plugin_slug ) );
|
||||
return array(
|
||||
Logger::info( 'Plugin bridge disabled', [ 'plugin' => $plugin_slug ] );
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __( '桥接已禁用', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __( '禁用失败', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -408,24 +393,24 @@ class BridgeManager {
|
|||
|
||||
// Agency 计划无限制
|
||||
if ( $subscription['plan'] === 'agency' ) {
|
||||
return array( 'allowed' => true );
|
||||
return [ 'allowed' => true ];
|
||||
}
|
||||
|
||||
$current_count = count( $this->get_bridged_plugins() );
|
||||
$limit = $subscription['plugins_limit'] ?? 5;
|
||||
|
||||
if ( $current_count >= $limit ) {
|
||||
return array(
|
||||
return [
|
||||
'allowed' => false,
|
||||
'message' => sprintf(
|
||||
/* translators: %d: plugin limit */
|
||||
__( '已达到插件数量限制 (%d),请升级订阅', 'wpbridge' ),
|
||||
$limit
|
||||
),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
return array( 'allowed' => true );
|
||||
return [ 'allowed' => true ];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -434,15 +419,15 @@ class BridgeManager {
|
|||
* @return array
|
||||
*/
|
||||
public function get_subscription(): array {
|
||||
$default = array(
|
||||
$default = [
|
||||
'plan' => 'free',
|
||||
'plugins_limit' => 0,
|
||||
'site_limit' => 1,
|
||||
'status' => 'active',
|
||||
'expires_at' => null,
|
||||
);
|
||||
];
|
||||
|
||||
$subscription = $this->settings->get( 'subscription', array() );
|
||||
$subscription = $this->settings->get( 'subscription', [] );
|
||||
return array_merge( $default, $subscription );
|
||||
}
|
||||
|
||||
|
|
@ -456,14 +441,14 @@ class BridgeManager {
|
|||
$bridged = $this->get_bridged_plugins();
|
||||
$available = $this->get_available_plugins();
|
||||
|
||||
return array(
|
||||
return [
|
||||
'bridged_count' => count( $bridged ),
|
||||
'available_count' => count( $available ),
|
||||
'plan' => $subscription['plan'],
|
||||
'plugins_limit' => $subscription['plugins_limit'],
|
||||
'plugins_used' => count( $bridged ),
|
||||
'can_add_more' => $subscription['plan'] === 'agency' || count( $bridged ) < $subscription['plugins_limit'],
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -479,7 +464,7 @@ class BridgeManager {
|
|||
$all_plugins = get_plugins();
|
||||
$available = $this->get_available_plugins();
|
||||
$bridged = $this->get_bridged_plugins();
|
||||
$result = array();
|
||||
$result = [];
|
||||
|
||||
foreach ( $all_plugins as $file => $data ) {
|
||||
$slug = dirname( $file );
|
||||
|
|
@ -490,14 +475,14 @@ class BridgeManager {
|
|||
if ( isset( $available[ $slug ] ) ) {
|
||||
$gpl_result = $this->gpl_validator->validate( $slug, $file );
|
||||
|
||||
$result[ $slug ] = array(
|
||||
$result[ $slug ] = [
|
||||
'file' => $file,
|
||||
'name' => $data['Name'],
|
||||
'version' => $data['Version'],
|
||||
'is_bridged' => in_array( $slug, $bridged, true ),
|
||||
'gpl_status' => $gpl_result,
|
||||
'available' => $available[ $slug ],
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -532,16 +517,16 @@ class BridgeManager {
|
|||
string $consumer_key,
|
||||
string $consumer_secret
|
||||
): array {
|
||||
$vendors = $this->settings->get( 'vendors', array() );
|
||||
$vendors = $this->settings->get( 'vendors', [] );
|
||||
|
||||
if ( isset( $vendors[ $vendor_id ] ) ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __( '供应商 ID 已存在', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
$vendors[ $vendor_id ] = array(
|
||||
$vendors[ $vendor_id ] = [
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'api_url' => $api_url,
|
||||
|
|
@ -549,7 +534,7 @@ class BridgeManager {
|
|||
'consumer_secret' => $consumer_secret,
|
||||
'enabled' => true,
|
||||
'created_at' => time(),
|
||||
);
|
||||
];
|
||||
|
||||
$this->settings->set( 'vendors', $vendors );
|
||||
|
||||
|
|
@ -565,18 +550,12 @@ class BridgeManager {
|
|||
$this->vendor_manager->register( $vendor );
|
||||
}
|
||||
|
||||
Logger::info(
|
||||
'Vendor added',
|
||||
array(
|
||||
'vendor_id' => $vendor_id,
|
||||
'type' => $type,
|
||||
)
|
||||
);
|
||||
Logger::info( 'Vendor added', [ 'vendor_id' => $vendor_id, 'type' => $type ] );
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __( '供应商已添加', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -586,25 +565,25 @@ class BridgeManager {
|
|||
* @return array
|
||||
*/
|
||||
public function remove_vendor( string $vendor_id ): array {
|
||||
$vendors = $this->settings->get( 'vendors', array() );
|
||||
$vendors = $this->settings->get( 'vendors', [] );
|
||||
|
||||
if ( ! isset( $vendors[ $vendor_id ] ) ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __( '供应商不存在', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
unset( $vendors[ $vendor_id ] );
|
||||
$this->settings->set( 'vendors', $vendors );
|
||||
$this->vendor_manager->unregister( $vendor_id );
|
||||
|
||||
Logger::info( 'Vendor removed', array( 'vendor_id' => $vendor_id ) );
|
||||
Logger::info( 'Vendor removed', [ 'vendor_id' => $vendor_id ] );
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __( '供应商已移除', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -613,7 +592,7 @@ class BridgeManager {
|
|||
* @return array
|
||||
*/
|
||||
public function get_vendors(): array {
|
||||
return $this->settings->get( 'vendors', array() );
|
||||
return $this->settings->get( 'vendors', [] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -626,20 +605,20 @@ class BridgeManager {
|
|||
$vendor = $this->vendor_manager->get( $vendor_id );
|
||||
|
||||
if ( ! $vendor ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __( '供应商不存在或未启用', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
$result = $vendor->test_connection();
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => $result,
|
||||
'message' => $result
|
||||
? __( '连接成功', 'wpbridge' )
|
||||
: __( '连接失败', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -650,23 +629,20 @@ class BridgeManager {
|
|||
* @return array
|
||||
*/
|
||||
public function add_custom_plugin( string $plugin_slug, array $info ): array {
|
||||
$custom = $this->settings->get( 'custom_plugins', array() );
|
||||
$custom = $this->settings->get( 'custom_plugins', [] );
|
||||
|
||||
$custom[ $plugin_slug ] = array_merge(
|
||||
$info,
|
||||
array(
|
||||
'added_at' => time(),
|
||||
)
|
||||
);
|
||||
$custom[ $plugin_slug ] = array_merge( $info, [
|
||||
'added_at' => time(),
|
||||
] );
|
||||
|
||||
$this->settings->set( 'custom_plugins', $custom );
|
||||
|
||||
Logger::info( 'Custom plugin added', array( 'plugin' => $plugin_slug ) );
|
||||
Logger::info( 'Custom plugin added', [ 'plugin' => $plugin_slug ] );
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __( '自定义插件已添加', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -676,21 +652,21 @@ class BridgeManager {
|
|||
* @return array
|
||||
*/
|
||||
public function remove_custom_plugin( string $plugin_slug ): array {
|
||||
$custom = $this->settings->get( 'custom_plugins', array() );
|
||||
$custom = $this->settings->get( 'custom_plugins', [] );
|
||||
|
||||
if ( ! isset( $custom[ $plugin_slug ] ) ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __( '自定义插件不存在', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
unset( $custom[ $plugin_slug ] );
|
||||
$this->settings->set( 'custom_plugins', $custom );
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __( '自定义插件已移除', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use WPBridge\Security\Validator;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,397 +22,370 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class CommercialManager {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 已注册的商业插件
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $registered_plugins = array();
|
||||
/**
|
||||
* 已注册的商业插件
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $registered_plugins = [];
|
||||
|
||||
/**
|
||||
* 版本锁定列表
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $version_locks = array();
|
||||
/**
|
||||
* 版本锁定列表
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $version_locks = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->version_locks = $this->settings->get( 'version_locks', array() );
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->version_locks = $this->settings->get( 'version_locks', [] );
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'filter_updates' ), 100 );
|
||||
add_action( 'upgrader_process_complete', array( $this, 'on_upgrade_complete' ), 10, 2 );
|
||||
}
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'filter_updates' ], 100 );
|
||||
add_action( 'upgrader_process_complete', [ $this, 'on_upgrade_complete' ], 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册商业插件
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @param array $config 配置
|
||||
*/
|
||||
public function register( string $slug, array $config ): void {
|
||||
$this->registered_plugins[ $slug ] = wp_parse_args(
|
||||
$config,
|
||||
array(
|
||||
'name' => $slug,
|
||||
'license_type' => 'unknown',
|
||||
'update_source' => '',
|
||||
'backup_enabled' => true,
|
||||
)
|
||||
);
|
||||
/**
|
||||
* 注册商业插件
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @param array $config 配置
|
||||
*/
|
||||
public function register( string $slug, array $config ): void {
|
||||
$this->registered_plugins[ $slug ] = wp_parse_args( $config, [
|
||||
'name' => $slug,
|
||||
'license_type' => 'unknown',
|
||||
'update_source' => '',
|
||||
'backup_enabled' => true,
|
||||
] );
|
||||
|
||||
Logger::debug( '注册商业插件', array( 'slug' => $slug ) );
|
||||
}
|
||||
Logger::debug( '注册商业插件', [ 'slug' => $slug ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测已安装的商业插件
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function detect_commercial_plugins(): array {
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
/**
|
||||
* 检测已安装的商业插件
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function detect_commercial_plugins(): array {
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
|
||||
$all_plugins = get_plugins();
|
||||
$commercial_plugins = array();
|
||||
$all_plugins = get_plugins();
|
||||
$commercial_plugins = [];
|
||||
|
||||
foreach ( $all_plugins as $file => $data ) {
|
||||
$slug = dirname( $file );
|
||||
foreach ( $all_plugins as $file => $data ) {
|
||||
$slug = dirname( $file );
|
||||
|
||||
// 检测商业插件特征
|
||||
if ( $this->is_commercial_plugin( $file, $data ) ) {
|
||||
$commercial_plugins[ $slug ] = array(
|
||||
'file' => $file,
|
||||
'name' => $data['Name'],
|
||||
'version' => $data['Version'],
|
||||
'license_type' => $this->detect_license_type( $file, $data ),
|
||||
'registered' => isset( $this->registered_plugins[ $slug ] ),
|
||||
);
|
||||
}
|
||||
}
|
||||
// 检测商业插件特征
|
||||
if ( $this->is_commercial_plugin( $file, $data ) ) {
|
||||
$commercial_plugins[ $slug ] = [
|
||||
'file' => $file,
|
||||
'name' => $data['Name'],
|
||||
'version' => $data['Version'],
|
||||
'license_type' => $this->detect_license_type( $file, $data ),
|
||||
'registered' => isset( $this->registered_plugins[ $slug ] ),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $commercial_plugins;
|
||||
}
|
||||
return $commercial_plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是商业插件
|
||||
*
|
||||
* @param string $file 插件文件
|
||||
* @param array $data 插件数据
|
||||
* @return bool
|
||||
*/
|
||||
private function is_commercial_plugin( string $file, array $data ): bool {
|
||||
// 已知商业插件列表(允许通过过滤器扩展)
|
||||
$known_commercial = apply_filters(
|
||||
'wpbridge_known_commercial_plugins',
|
||||
array(
|
||||
'elementor-pro',
|
||||
'wordpress-seo-premium',
|
||||
'seo-by-rank-math-pro',
|
||||
'advanced-custom-fields-pro',
|
||||
'gravityforms',
|
||||
'wpforms',
|
||||
'ninja-forms',
|
||||
'woocommerce-subscriptions',
|
||||
'woocommerce-memberships',
|
||||
'learndash',
|
||||
'memberpress',
|
||||
'wpml-sitepress-multilingual-cms',
|
||||
'updraftplus-premium',
|
||||
'wp-rocket',
|
||||
'perfmatters',
|
||||
)
|
||||
);
|
||||
/**
|
||||
* 检查是否是商业插件
|
||||
*
|
||||
* @param string $file 插件文件
|
||||
* @param array $data 插件数据
|
||||
* @return bool
|
||||
*/
|
||||
private function is_commercial_plugin( string $file, array $data ): bool {
|
||||
// 已知商业插件列表(允许通过过滤器扩展)
|
||||
$known_commercial = apply_filters( 'wpbridge_known_commercial_plugins', [
|
||||
'elementor-pro',
|
||||
'wordpress-seo-premium',
|
||||
'seo-by-rank-math-pro',
|
||||
'advanced-custom-fields-pro',
|
||||
'gravityforms',
|
||||
'wpforms',
|
||||
'ninja-forms',
|
||||
'woocommerce-subscriptions',
|
||||
'woocommerce-memberships',
|
||||
'learndash',
|
||||
'memberpress',
|
||||
'wpml-sitepress-multilingual-cms',
|
||||
'updraftplus-premium',
|
||||
'wp-rocket',
|
||||
'perfmatters',
|
||||
] );
|
||||
|
||||
$slug = dirname( $file );
|
||||
$slug = dirname( $file );
|
||||
|
||||
if ( in_array( $slug, $known_commercial, true ) ) {
|
||||
return true;
|
||||
}
|
||||
if ( in_array( $slug, $known_commercial, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查插件头信息
|
||||
$plugin_path = WP_PLUGIN_DIR . '/' . $file;
|
||||
// 检查插件头信息
|
||||
$plugin_path = WP_PLUGIN_DIR . '/' . $file;
|
||||
|
||||
// 路径安全验证:防止路径遍历
|
||||
$real_path = realpath( $plugin_path );
|
||||
$plugin_dir_real = realpath( WP_PLUGIN_DIR );
|
||||
// 路径安全验证:防止路径遍历
|
||||
$real_path = realpath( $plugin_path );
|
||||
$plugin_dir_real = realpath( WP_PLUGIN_DIR );
|
||||
|
||||
if ( ! $real_path || ! $plugin_dir_real || strpos( $real_path, $plugin_dir_real . DIRECTORY_SEPARATOR ) !== 0 ) {
|
||||
return false; // 路径不安全,跳过
|
||||
}
|
||||
if ( ! $real_path || ! $plugin_dir_real || strpos( $real_path, $plugin_dir_real . DIRECTORY_SEPARATOR ) !== 0 ) {
|
||||
return false; // 路径不安全,跳过
|
||||
}
|
||||
|
||||
if ( file_exists( $real_path ) ) {
|
||||
$content = file_get_contents( $real_path, false, null, 0, 8192 );
|
||||
if ( file_exists( $real_path ) ) {
|
||||
$content = file_get_contents( $real_path, false, null, 0, 8192 );
|
||||
|
||||
// 检查授权相关关键词
|
||||
$license_keywords = array(
|
||||
'license_key',
|
||||
'license-key',
|
||||
'activation_key',
|
||||
'purchase_code',
|
||||
'envato',
|
||||
'codecanyon',
|
||||
'themeforest',
|
||||
);
|
||||
// 检查授权相关关键词
|
||||
$license_keywords = [
|
||||
'license_key',
|
||||
'license-key',
|
||||
'activation_key',
|
||||
'purchase_code',
|
||||
'envato',
|
||||
'codecanyon',
|
||||
'themeforest',
|
||||
];
|
||||
|
||||
foreach ( $license_keywords as $keyword ) {
|
||||
if ( stripos( $content, $keyword ) !== false ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ( $license_keywords as $keyword ) {
|
||||
if ( stripos( $content, $keyword ) !== false ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测授权类型
|
||||
*
|
||||
* @param string $file 插件文件
|
||||
* @param array $data 插件数据
|
||||
* @return string
|
||||
*/
|
||||
private function detect_license_type( string $file, array $data ): string {
|
||||
$plugin_path = WP_PLUGIN_DIR . '/' . $file;
|
||||
/**
|
||||
* 检测授权类型
|
||||
*
|
||||
* @param string $file 插件文件
|
||||
* @param array $data 插件数据
|
||||
* @return string
|
||||
*/
|
||||
private function detect_license_type( string $file, array $data ): string {
|
||||
$plugin_path = WP_PLUGIN_DIR . '/' . $file;
|
||||
|
||||
if ( ! file_exists( $plugin_path ) ) {
|
||||
return 'unknown';
|
||||
}
|
||||
if ( ! file_exists( $plugin_path ) ) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
$content = file_get_contents( $plugin_path, false, null, 0, 8192 );
|
||||
$content = file_get_contents( $plugin_path, false, null, 0, 8192 );
|
||||
|
||||
// EDD Software Licensing
|
||||
if ( stripos( $content, 'EDD_SL_Plugin_Updater' ) !== false ) {
|
||||
return 'edd';
|
||||
}
|
||||
// EDD Software Licensing
|
||||
if ( stripos( $content, 'EDD_SL_Plugin_Updater' ) !== false ) {
|
||||
return 'edd';
|
||||
}
|
||||
|
||||
// WooCommerce API Manager
|
||||
if ( stripos( $content, 'WC_AM_Client' ) !== false ) {
|
||||
return 'woocommerce';
|
||||
}
|
||||
// WooCommerce API Manager
|
||||
if ( stripos( $content, 'WC_AM_Client' ) !== false ) {
|
||||
return 'woocommerce';
|
||||
}
|
||||
|
||||
// Envato
|
||||
if ( stripos( $content, 'envato' ) !== false ) {
|
||||
return 'envato';
|
||||
}
|
||||
// Envato
|
||||
if ( stripos( $content, 'envato' ) !== false ) {
|
||||
return 'envato';
|
||||
}
|
||||
|
||||
// WPML
|
||||
if ( stripos( $content, 'OTGS' ) !== false ) {
|
||||
return 'otgs';
|
||||
}
|
||||
// WPML
|
||||
if ( stripos( $content, 'OTGS' ) !== false ) {
|
||||
return 'otgs';
|
||||
}
|
||||
|
||||
return 'custom';
|
||||
}
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤更新
|
||||
*
|
||||
* @param object $transient 更新 transient
|
||||
* @return object
|
||||
*/
|
||||
public function filter_updates( $transient ) {
|
||||
if ( empty( $transient->response ) ) {
|
||||
return $transient;
|
||||
}
|
||||
/**
|
||||
* 过滤更新
|
||||
*
|
||||
* @param object $transient 更新 transient
|
||||
* @return object
|
||||
*/
|
||||
public function filter_updates( $transient ) {
|
||||
if ( empty( $transient->response ) ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
foreach ( $transient->response as $file => $update ) {
|
||||
$slug = dirname( $file );
|
||||
foreach ( $transient->response as $file => $update ) {
|
||||
$slug = dirname( $file );
|
||||
|
||||
// 检查版本锁定
|
||||
if ( $this->is_version_locked( $slug ) ) {
|
||||
$locked_version = $this->get_locked_version( $slug );
|
||||
// 检查版本锁定
|
||||
if ( $this->is_version_locked( $slug ) ) {
|
||||
$locked_version = $this->get_locked_version( $slug );
|
||||
|
||||
if ( version_compare( $update->new_version, $locked_version, '>' ) ) {
|
||||
Logger::info(
|
||||
'版本锁定阻止更新',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'locked_version' => $locked_version,
|
||||
'new_version' => $update->new_version,
|
||||
)
|
||||
);
|
||||
if ( version_compare( $update->new_version, $locked_version, '>' ) ) {
|
||||
Logger::info( '版本锁定阻止更新', [
|
||||
'slug' => $slug,
|
||||
'locked_version' => $locked_version,
|
||||
'new_version' => $update->new_version,
|
||||
] );
|
||||
|
||||
unset( $transient->response[ $file ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
unset( $transient->response[ $file ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $transient;
|
||||
}
|
||||
return $transient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定版本
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $version 版本号
|
||||
* @return bool
|
||||
*/
|
||||
public function lock_version( string $slug, string $version ): bool {
|
||||
// 权限检查
|
||||
if ( ! current_user_can( 'update_plugins' ) ) {
|
||||
Logger::warning(
|
||||
'无权限锁定版本',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'user' => get_current_user_id(),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 锁定版本
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $version 版本号
|
||||
* @return bool
|
||||
*/
|
||||
public function lock_version( string $slug, string $version ): bool {
|
||||
// 权限检查
|
||||
if ( ! current_user_can( 'update_plugins' ) ) {
|
||||
Logger::warning( '无权限锁定版本', [ 'slug' => $slug, 'user' => get_current_user_id() ] );
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证版本号格式
|
||||
if ( ! Validator::is_valid_version( $version ) ) {
|
||||
return false;
|
||||
}
|
||||
// 验证版本号格式
|
||||
if ( ! Validator::is_valid_version( $version ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->version_locks[ $slug ] = array(
|
||||
'version' => sanitize_text_field( $version ),
|
||||
'locked_at' => current_time( 'mysql' ),
|
||||
'locked_by' => get_current_user_id(),
|
||||
);
|
||||
$this->version_locks[ $slug ] = [
|
||||
'version' => sanitize_text_field( $version ),
|
||||
'locked_at' => current_time( 'mysql' ),
|
||||
'locked_by' => get_current_user_id(),
|
||||
];
|
||||
|
||||
$result = $this->settings->set( 'version_locks', $this->version_locks );
|
||||
$result = $this->settings->set( 'version_locks', $this->version_locks );
|
||||
|
||||
if ( $result ) {
|
||||
Logger::info(
|
||||
'版本已锁定',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'version' => $version,
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $result ) {
|
||||
Logger::info( '版本已锁定', [ 'slug' => $slug, 'version' => $version ] );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解锁版本
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return bool
|
||||
*/
|
||||
public function unlock_version( string $slug ): bool {
|
||||
// 权限检查
|
||||
if ( ! current_user_can( 'update_plugins' ) ) {
|
||||
Logger::warning(
|
||||
'无权限解锁版本',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'user' => get_current_user_id(),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 解锁版本
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return bool
|
||||
*/
|
||||
public function unlock_version( string $slug ): bool {
|
||||
// 权限检查
|
||||
if ( ! current_user_can( 'update_plugins' ) ) {
|
||||
Logger::warning( '无权限解锁版本', [ 'slug' => $slug, 'user' => get_current_user_id() ] );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $this->version_locks[ $slug ] ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( ! isset( $this->version_locks[ $slug ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset( $this->version_locks[ $slug ] );
|
||||
unset( $this->version_locks[ $slug ] );
|
||||
|
||||
$result = $this->settings->set( 'version_locks', $this->version_locks );
|
||||
$result = $this->settings->set( 'version_locks', $this->version_locks );
|
||||
|
||||
if ( $result ) {
|
||||
Logger::info( '版本已解锁', array( 'slug' => $slug ) );
|
||||
}
|
||||
if ( $result ) {
|
||||
Logger::info( '版本已解锁', [ 'slug' => $slug ] );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查版本是否锁定
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return bool
|
||||
*/
|
||||
public function is_version_locked( string $slug ): bool {
|
||||
return isset( $this->version_locks[ $slug ] );
|
||||
}
|
||||
/**
|
||||
* 检查版本是否锁定
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return bool
|
||||
*/
|
||||
public function is_version_locked( string $slug ): bool {
|
||||
return isset( $this->version_locks[ $slug ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁定的版本
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_locked_version( string $slug ): ?string {
|
||||
return $this->version_locks[ $slug ]['version'] ?? null;
|
||||
}
|
||||
/**
|
||||
* 获取锁定的版本
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return string|null
|
||||
*/
|
||||
public function get_locked_version( string $slug ): ?string {
|
||||
return $this->version_locks[ $slug ]['version'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有版本锁定
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_version_locks(): array {
|
||||
return $this->version_locks;
|
||||
}
|
||||
/**
|
||||
* 获取所有版本锁定
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_version_locks(): array {
|
||||
return $this->version_locks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新完成时触发
|
||||
*
|
||||
* @param \WP_Upgrader $upgrader 升级器
|
||||
* @param array $options 选项
|
||||
*/
|
||||
public function on_upgrade_complete( $upgrader, array $options ): void {
|
||||
if ( $options['type'] !== 'plugin' || $options['action'] !== 'update' ) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 更新完成时触发
|
||||
*
|
||||
* @param \WP_Upgrader $upgrader 升级器
|
||||
* @param array $options 选项
|
||||
*/
|
||||
public function on_upgrade_complete( $upgrader, array $options ): void {
|
||||
if ( $options['type'] !== 'plugin' || $options['action'] !== 'update' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$plugins = $options['plugins'] ?? array();
|
||||
$plugins = $options['plugins'] ?? [];
|
||||
|
||||
foreach ( $plugins as $file ) {
|
||||
$slug = dirname( $file );
|
||||
foreach ( $plugins as $file ) {
|
||||
$slug = dirname( $file );
|
||||
|
||||
// 触发更新完成事件
|
||||
do_action( 'wpbridge_plugin_updated', $slug, $file );
|
||||
// 触发更新完成事件
|
||||
do_action( 'wpbridge_plugin_updated', $slug, $file );
|
||||
|
||||
Logger::info( '插件更新完成', array( 'slug' => $slug ) );
|
||||
}
|
||||
}
|
||||
Logger::info( '插件更新完成', [ 'slug' => $slug ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已注册的商业插件
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_registered_plugins(): array {
|
||||
return $this->registered_plugins;
|
||||
}
|
||||
/**
|
||||
* 获取已注册的商业插件
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_registered_plugins(): array {
|
||||
return $this->registered_plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
$detected = $this->detect_commercial_plugins();
|
||||
/**
|
||||
* 获取统计信息
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
$detected = $this->detect_commercial_plugins();
|
||||
|
||||
return array(
|
||||
'detected_count' => count( $detected ),
|
||||
'registered_count' => count( $this->registered_plugins ),
|
||||
'locked_count' => count( $this->version_locks ),
|
||||
);
|
||||
}
|
||||
return [
|
||||
'detected_count' => count( $detected ),
|
||||
'registered_count' => count( $this->registered_plugins ),
|
||||
'locked_count' => count( $this->version_locks ),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class GPLValidator {
|
|||
/**
|
||||
* GPL 兼容的授权标识
|
||||
*/
|
||||
private const GPL_COMPATIBLE_LICENSES = array(
|
||||
private const GPL_COMPATIBLE_LICENSES = [
|
||||
'gpl',
|
||||
'gpl-2.0',
|
||||
'gpl-2.0+',
|
||||
|
|
@ -45,12 +45,12 @@ class GPLValidator {
|
|||
'mit',
|
||||
'apache-2.0',
|
||||
'bsd',
|
||||
);
|
||||
];
|
||||
|
||||
/**
|
||||
* 已知的 GPL 商业插件列表
|
||||
*/
|
||||
private const KNOWN_GPL_PLUGINS = array(
|
||||
private const KNOWN_GPL_PLUGINS = [
|
||||
'elementor-pro',
|
||||
'wordpress-seo-premium',
|
||||
'advanced-custom-fields-pro',
|
||||
|
|
@ -68,21 +68,21 @@ class GPLValidator {
|
|||
'learndash',
|
||||
'woocommerce-subscriptions',
|
||||
'woocommerce-memberships',
|
||||
);
|
||||
];
|
||||
|
||||
/**
|
||||
* 已知的非 GPL 插件列表(不应桥接)
|
||||
*/
|
||||
private const NON_GPL_PLUGINS = array(
|
||||
private const NON_GPL_PLUGINS = [
|
||||
// Envato 独占插件通常不是 GPL
|
||||
);
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证结果缓存
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $cache = array();
|
||||
private array $cache = [];
|
||||
|
||||
/**
|
||||
* 验证插件是否 GPL 兼容
|
||||
|
|
@ -115,32 +115,32 @@ class GPLValidator {
|
|||
private function do_validate( string $plugin_slug, string $plugin_file ): array {
|
||||
// 1. 检查已知列表
|
||||
if ( in_array( $plugin_slug, self::KNOWN_GPL_PLUGINS, true ) ) {
|
||||
return array(
|
||||
return [
|
||||
'is_gpl' => true,
|
||||
'confidence' => 100,
|
||||
'source' => 'known_list',
|
||||
'license' => 'GPL-2.0+',
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
if ( in_array( $plugin_slug, self::NON_GPL_PLUGINS, true ) ) {
|
||||
return array(
|
||||
return [
|
||||
'is_gpl' => false,
|
||||
'confidence' => 100,
|
||||
'source' => 'known_list',
|
||||
'license' => 'proprietary',
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
// 2. 检查 WordPress.org(如果存在则一定是 GPL)
|
||||
$wporg_result = $this->check_wordpress_org( $plugin_slug );
|
||||
if ( $wporg_result !== null ) {
|
||||
return array(
|
||||
return [
|
||||
'is_gpl' => true,
|
||||
'confidence' => 100,
|
||||
'source' => 'wordpress_org',
|
||||
'license' => $wporg_result['license'] ?? 'GPL-2.0+',
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
// 3. 检查插件文件
|
||||
|
|
@ -152,12 +152,12 @@ class GPLValidator {
|
|||
}
|
||||
|
||||
// 4. 无法确定
|
||||
return array(
|
||||
return [
|
||||
'is_gpl' => null,
|
||||
'confidence' => 0,
|
||||
'source' => 'unknown',
|
||||
'license' => 'unknown',
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -175,7 +175,7 @@ class GPLValidator {
|
|||
}
|
||||
|
||||
$url = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=' . urlencode( $plugin_slug );
|
||||
$response = wp_remote_get( $url, array( 'timeout' => 5 ) );
|
||||
$response = wp_remote_get( $url, [ 'timeout' => 5 ] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return null;
|
||||
|
|
@ -187,10 +187,10 @@ class GPLValidator {
|
|||
if ( $code === 200 && ! empty( $body ) ) {
|
||||
$data = json_decode( $body, true );
|
||||
if ( isset( $data['slug'] ) ) {
|
||||
$result = array(
|
||||
$result = [
|
||||
'license' => 'GPL-2.0+', // WordPress.org 要求 GPL
|
||||
'name' => $data['name'] ?? '',
|
||||
);
|
||||
];
|
||||
set_transient( $cache_key, $result, DAY_IN_SECONDS );
|
||||
return $result;
|
||||
}
|
||||
|
|
@ -224,28 +224,28 @@ class GPLValidator {
|
|||
if ( ! empty( $license ) ) {
|
||||
$is_gpl = $this->is_gpl_compatible_license( $license );
|
||||
if ( $is_gpl !== null ) {
|
||||
return array(
|
||||
return [
|
||||
'is_gpl' => $is_gpl,
|
||||
'confidence' => 90,
|
||||
'source' => 'plugin_header',
|
||||
'license' => $license,
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 license.txt
|
||||
$plugin_dir = dirname( $plugin_path );
|
||||
$plugin_dir = dirname( $plugin_path );
|
||||
$license_file = $plugin_dir . '/license.txt';
|
||||
|
||||
if ( file_exists( $license_file ) ) {
|
||||
$license_content = file_get_contents( $license_file );
|
||||
if ( $this->contains_gpl_text( $license_content ) ) {
|
||||
return array(
|
||||
return [
|
||||
'is_gpl' => true,
|
||||
'confidence' => 85,
|
||||
'source' => 'license_file',
|
||||
'license' => 'GPL (from license.txt)',
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -257,12 +257,12 @@ class GPLValidator {
|
|||
$license = trim( $matches[1] );
|
||||
$is_gpl = $this->is_gpl_compatible_license( $license );
|
||||
if ( $is_gpl !== null ) {
|
||||
return array(
|
||||
return [
|
||||
'is_gpl' => $is_gpl,
|
||||
'confidence' => 80,
|
||||
'source' => 'readme_file',
|
||||
'license' => $license,
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -286,7 +286,7 @@ class GPLValidator {
|
|||
}
|
||||
|
||||
// 检查明确的非 GPL 标识
|
||||
$non_gpl_indicators = array( 'proprietary', 'commercial', 'all rights reserved', 'envato' );
|
||||
$non_gpl_indicators = [ 'proprietary', 'commercial', 'all rights reserved', 'envato' ];
|
||||
foreach ( $non_gpl_indicators as $indicator ) {
|
||||
if ( strpos( $license_lower, $indicator ) !== false ) {
|
||||
return false;
|
||||
|
|
@ -303,7 +303,7 @@ class GPLValidator {
|
|||
* @return bool
|
||||
*/
|
||||
private function contains_gpl_text( string $content ): bool {
|
||||
$gpl_indicators = array(
|
||||
$gpl_indicators = [
|
||||
'GNU General Public License',
|
||||
'GPL version 2',
|
||||
'GPL version 3',
|
||||
|
|
@ -311,7 +311,7 @@ class GPLValidator {
|
|||
'GPLv3',
|
||||
'free software',
|
||||
'redistribute it and/or modify',
|
||||
);
|
||||
];
|
||||
|
||||
foreach ( $gpl_indicators as $indicator ) {
|
||||
if ( stripos( $content, $indicator ) !== false ) {
|
||||
|
|
@ -329,7 +329,7 @@ class GPLValidator {
|
|||
* @return array
|
||||
*/
|
||||
public function validate_batch( array $plugins ): array {
|
||||
$results = array();
|
||||
$results = [];
|
||||
foreach ( $plugins as $slug => $file ) {
|
||||
$results[ $slug ] = $this->validate( $slug, $file );
|
||||
}
|
||||
|
|
@ -340,7 +340,7 @@ class GPLValidator {
|
|||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->cache = array();
|
||||
$this->cache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -349,11 +349,11 @@ class GPLValidator {
|
|||
* @param string $plugin_slug 插件 slug
|
||||
*/
|
||||
public function add_known_gpl( string $plugin_slug ): void {
|
||||
$this->cache[ $plugin_slug ] = array(
|
||||
$this->cache[ $plugin_slug ] = [
|
||||
'is_gpl' => true,
|
||||
'confidence' => 100,
|
||||
'source' => 'manual',
|
||||
'license' => 'GPL (manually verified)',
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,40 +28,40 @@ class LicenseProxy {
|
|||
/**
|
||||
* 支持的授权系统配置
|
||||
*/
|
||||
private const VENDORS = array(
|
||||
'edd' => array(
|
||||
private const VENDORS = [
|
||||
'edd' => [
|
||||
'name' => 'EDD Software Licensing',
|
||||
'patterns' => array(
|
||||
'patterns' => [
|
||||
'/edd-sl/',
|
||||
'/edd-api/',
|
||||
'action=activate_license',
|
||||
'action=check_license',
|
||||
'action=deactivate_license',
|
||||
),
|
||||
],
|
||||
'response_format' => 'edd',
|
||||
),
|
||||
'freemius' => array(
|
||||
],
|
||||
'freemius' => [
|
||||
'name' => 'Freemius',
|
||||
'patterns' => array(
|
||||
'patterns' => [
|
||||
'api.freemius.com',
|
||||
'wp-json/freemius',
|
||||
),
|
||||
],
|
||||
'response_format' => 'freemius',
|
||||
),
|
||||
'wc_am' => array(
|
||||
],
|
||||
'wc_am' => [
|
||||
'name' => 'WooCommerce API Manager',
|
||||
'patterns' => array(
|
||||
'patterns' => [
|
||||
'wc-api/wc-am-api',
|
||||
'wc-api/am-software-api',
|
||||
),
|
||||
],
|
||||
'response_format' => 'wc_am',
|
||||
),
|
||||
);
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 敏感参数列表(用于日志过滤)
|
||||
*/
|
||||
private const SENSITIVE_PARAMS = array(
|
||||
private const SENSITIVE_PARAMS = [
|
||||
'license_key',
|
||||
'license',
|
||||
'key',
|
||||
|
|
@ -70,7 +70,7 @@ class LicenseProxy {
|
|||
'token',
|
||||
'api_key',
|
||||
'apikey',
|
||||
);
|
||||
];
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
|
|
@ -84,7 +84,7 @@ class LicenseProxy {
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $bridged_plugins = array();
|
||||
private array $bridged_plugins = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
|
|
@ -93,7 +93,7 @@ class LicenseProxy {
|
|||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->bridged_plugins = $this->settings->get( 'bridged_plugins', array() );
|
||||
$this->bridged_plugins = $this->settings->get( 'bridged_plugins', [] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -103,7 +103,7 @@ class LicenseProxy {
|
|||
if ( ! $this->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
add_filter( 'pre_http_request', array( $this, 'intercept_request' ), 5, 3 );
|
||||
add_filter( 'pre_http_request', [ $this, 'intercept_request' ], 5, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -142,14 +142,11 @@ class LicenseProxy {
|
|||
}
|
||||
|
||||
// H2 修复: 过滤敏感信息后再记录日志
|
||||
Logger::debug(
|
||||
'License proxy intercepting',
|
||||
array(
|
||||
'vendor' => $vendor,
|
||||
'plugin' => $plugin_slug,
|
||||
'url' => $this->sanitize_url_for_log( $url ),
|
||||
)
|
||||
);
|
||||
Logger::debug( 'License proxy intercepting', [
|
||||
'vendor' => $vendor,
|
||||
'plugin' => $plugin_slug,
|
||||
'url' => $this->sanitize_url_for_log( $url ),
|
||||
] );
|
||||
|
||||
// 4. 代理到文派服务
|
||||
return $this->proxy_request( $vendor, $plugin_slug, $url, $args );
|
||||
|
|
@ -173,7 +170,7 @@ class LicenseProxy {
|
|||
* @return array 过滤后的请求体
|
||||
*/
|
||||
private function sanitize_body_for_log( array $body ): array {
|
||||
$sanitized = array();
|
||||
$sanitized = [];
|
||||
foreach ( $body as $key => $value ) {
|
||||
if ( in_array( strtolower( $key ), self::SENSITIVE_PARAMS, true ) ) {
|
||||
$sanitized[ $key ] = '[REDACTED]';
|
||||
|
|
@ -250,7 +247,7 @@ class LicenseProxy {
|
|||
*/
|
||||
private function resolve_item_id( string $item_id ): ?string {
|
||||
// 从远程配置获取 ID 到 slug 的映射
|
||||
$mapping = $this->settings->get( 'item_id_mapping', array() );
|
||||
$mapping = $this->settings->get( 'item_id_mapping', [] );
|
||||
return $mapping[ $item_id ] ?? null;
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +258,7 @@ class LicenseProxy {
|
|||
* @return string|null
|
||||
*/
|
||||
private function resolve_freemius_id( string $freemius_id ): ?string {
|
||||
$mapping = $this->settings->get( 'freemius_id_mapping', array() );
|
||||
$mapping = $this->settings->get( 'freemius_id_mapping', [] );
|
||||
return $mapping[ $freemius_id ] ?? null;
|
||||
}
|
||||
|
||||
|
|
@ -283,13 +280,13 @@ class LicenseProxy {
|
|||
* @return string
|
||||
*/
|
||||
private function generate_site_fingerprint(): string {
|
||||
$factors = array(
|
||||
$factors = [
|
||||
home_url(),
|
||||
defined( 'DB_NAME' ) ? DB_NAME : '',
|
||||
defined( 'AUTH_KEY' ) ? AUTH_KEY : '',
|
||||
defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : '',
|
||||
php_uname( 'n' ), // 主机名
|
||||
);
|
||||
];
|
||||
|
||||
return hash( 'sha256', implode( '|', $factors ) );
|
||||
}
|
||||
|
|
@ -304,15 +301,12 @@ class LicenseProxy {
|
|||
* @return string
|
||||
*/
|
||||
private function generate_request_signature( string $api_key, string $plugin_slug, string $action, string $timestamp ): string {
|
||||
$data = implode(
|
||||
'|',
|
||||
array(
|
||||
$plugin_slug,
|
||||
$this->generate_site_fingerprint(),
|
||||
$action,
|
||||
$timestamp,
|
||||
)
|
||||
);
|
||||
$data = implode( '|', [
|
||||
$plugin_slug,
|
||||
$this->generate_site_fingerprint(),
|
||||
$action,
|
||||
$timestamp,
|
||||
] );
|
||||
|
||||
return hash_hmac( 'sha256', $data, $api_key );
|
||||
}
|
||||
|
|
@ -340,36 +334,28 @@ class LicenseProxy {
|
|||
$site_fingerprint = $this->generate_site_fingerprint();
|
||||
$signature = $this->generate_request_signature( $api_key, $plugin_slug, $action, $timestamp );
|
||||
|
||||
$response = wp_remote_post(
|
||||
$proxy_url,
|
||||
array(
|
||||
'timeout' => 15,
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
'X-WPBridge-Key' => $api_key,
|
||||
'X-WPBridge-Signature' => $signature,
|
||||
'X-WPBridge-Timestamp' => $timestamp,
|
||||
'X-WPBridge-Fingerprint' => $site_fingerprint,
|
||||
),
|
||||
'body' => wp_json_encode(
|
||||
array(
|
||||
'vendor' => $vendor,
|
||||
'plugin_slug' => $plugin_slug,
|
||||
'action' => $action,
|
||||
'site_url' => home_url(),
|
||||
'site_fingerprint' => $site_fingerprint,
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
$response = wp_remote_post( $proxy_url, [
|
||||
'timeout' => 15,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-WPBridge-Key' => $api_key,
|
||||
'X-WPBridge-Signature' => $signature,
|
||||
'X-WPBridge-Timestamp' => $timestamp,
|
||||
'X-WPBridge-Fingerprint' => $site_fingerprint,
|
||||
],
|
||||
'body' => wp_json_encode( [
|
||||
'vendor' => $vendor,
|
||||
'plugin_slug' => $plugin_slug,
|
||||
'action' => $action,
|
||||
'site_url' => home_url(),
|
||||
'site_fingerprint' => $site_fingerprint,
|
||||
] ),
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error(
|
||||
'License proxy failed',
|
||||
array(
|
||||
'error' => $response->get_error_message(),
|
||||
)
|
||||
);
|
||||
Logger::error( 'License proxy failed', [
|
||||
'error' => $response->get_error_message(),
|
||||
] );
|
||||
// 失败时不拦截,让原始请求继续
|
||||
return false;
|
||||
}
|
||||
|
|
@ -393,7 +379,7 @@ class LicenseProxy {
|
|||
return false; // 让原始请求继续
|
||||
}
|
||||
|
||||
$license = $body['license'] ?? array();
|
||||
$license = $body['license'] ?? [];
|
||||
|
||||
// 根据不同授权系统返回不同格式
|
||||
switch ( $vendor ) {
|
||||
|
|
@ -419,35 +405,27 @@ class LicenseProxy {
|
|||
// 获取插件特定的响应配置
|
||||
$plugin_config = $this->get_plugin_response_config( $plugin_slug, 'edd' );
|
||||
|
||||
$body = wp_json_encode(
|
||||
array_merge(
|
||||
array(
|
||||
'success' => true,
|
||||
'license' => $license['status'] ?? 'valid',
|
||||
'item_id' => $license['item_id'] ?? '',
|
||||
'item_name' => $license['item_name'] ?? $plugin_config['item_name'] ?? '',
|
||||
'license_limit' => $license['license_limit'] ?? 0,
|
||||
'site_count' => $license['site_count'] ?? 1,
|
||||
'expires' => $license['expires'] ?? 'lifetime',
|
||||
'activations_left' => $license['activations_left'] ?? 'unlimited',
|
||||
'checksum' => $license['checksum'] ?? $this->generate_checksum( $license ),
|
||||
'payment_id' => $license['payment_id'] ?? 0,
|
||||
'customer_name' => $license['customer_name'] ?? '',
|
||||
'customer_email' => $license['customer_email'] ?? '',
|
||||
'price_id' => $license['price_id'] ?? false,
|
||||
),
|
||||
$plugin_config['extra_fields'] ?? array()
|
||||
)
|
||||
);
|
||||
$body = wp_json_encode( array_merge( [
|
||||
'success' => true,
|
||||
'license' => $license['status'] ?? 'valid',
|
||||
'item_id' => $license['item_id'] ?? '',
|
||||
'item_name' => $license['item_name'] ?? $plugin_config['item_name'] ?? '',
|
||||
'license_limit' => $license['license_limit'] ?? 0,
|
||||
'site_count' => $license['site_count'] ?? 1,
|
||||
'expires' => $license['expires'] ?? 'lifetime',
|
||||
'activations_left' => $license['activations_left'] ?? 'unlimited',
|
||||
'checksum' => $license['checksum'] ?? $this->generate_checksum( $license ),
|
||||
'payment_id' => $license['payment_id'] ?? 0,
|
||||
'customer_name' => $license['customer_name'] ?? '',
|
||||
'customer_email' => $license['customer_email'] ?? '',
|
||||
'price_id' => $license['price_id'] ?? false,
|
||||
], $plugin_config['extra_fields'] ?? [] ) );
|
||||
|
||||
return array(
|
||||
'response' => array(
|
||||
'code' => 200,
|
||||
'message' => 'OK',
|
||||
),
|
||||
return [
|
||||
'response' => [ 'code' => 200, 'message' => 'OK' ],
|
||||
'body' => $body,
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
);
|
||||
'headers' => [ 'content-type' => 'application/json' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -460,37 +438,29 @@ class LicenseProxy {
|
|||
private function format_freemius_response( array $license, string $plugin_slug ): array {
|
||||
$plugin_config = $this->get_plugin_response_config( $plugin_slug, 'freemius' );
|
||||
|
||||
$body = wp_json_encode(
|
||||
array_merge(
|
||||
array(
|
||||
'id' => $license['id'] ?? 0,
|
||||
'plugin_id' => $license['plugin_id'] ?? $plugin_config['plugin_id'] ?? 0,
|
||||
'user_id' => $license['user_id'] ?? 0,
|
||||
'plan_id' => $license['plan_id'] ?? $plugin_config['plan_id'] ?? 0,
|
||||
'pricing_id' => $license['pricing_id'] ?? 0,
|
||||
'quota' => $license['license_limit'] ?? null,
|
||||
'activated' => $license['site_count'] ?? 1,
|
||||
'activated_local' => 1,
|
||||
'expiration' => $license['expires'] ?? null,
|
||||
'secret_key' => $license['secret_key'] ?? $this->generate_secret_key(),
|
||||
'public_key' => $license['public_key'] ?? $plugin_config['public_key'] ?? '',
|
||||
'is_free_localhost' => false,
|
||||
'is_block_features' => false,
|
||||
'is_cancelled' => false,
|
||||
'is_whitelabeled' => $license['is_whitelabeled'] ?? false,
|
||||
),
|
||||
$plugin_config['extra_fields'] ?? array()
|
||||
)
|
||||
);
|
||||
$body = wp_json_encode( array_merge( [
|
||||
'id' => $license['id'] ?? 0,
|
||||
'plugin_id' => $license['plugin_id'] ?? $plugin_config['plugin_id'] ?? 0,
|
||||
'user_id' => $license['user_id'] ?? 0,
|
||||
'plan_id' => $license['plan_id'] ?? $plugin_config['plan_id'] ?? 0,
|
||||
'pricing_id' => $license['pricing_id'] ?? 0,
|
||||
'quota' => $license['license_limit'] ?? null,
|
||||
'activated' => $license['site_count'] ?? 1,
|
||||
'activated_local' => 1,
|
||||
'expiration' => $license['expires'] ?? null,
|
||||
'secret_key' => $license['secret_key'] ?? $this->generate_secret_key(),
|
||||
'public_key' => $license['public_key'] ?? $plugin_config['public_key'] ?? '',
|
||||
'is_free_localhost' => false,
|
||||
'is_block_features' => false,
|
||||
'is_cancelled' => false,
|
||||
'is_whitelabeled' => $license['is_whitelabeled'] ?? false,
|
||||
], $plugin_config['extra_fields'] ?? [] ) );
|
||||
|
||||
return array(
|
||||
'response' => array(
|
||||
'code' => 200,
|
||||
'message' => 'OK',
|
||||
),
|
||||
return [
|
||||
'response' => [ 'code' => 200, 'message' => 'OK' ],
|
||||
'body' => $body,
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
);
|
||||
'headers' => [ 'content-type' => 'application/json' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -500,25 +470,20 @@ class LicenseProxy {
|
|||
* @return array
|
||||
*/
|
||||
private function format_wc_am_response( array $license ): array {
|
||||
$body = wp_json_encode(
|
||||
array(
|
||||
'success' => true,
|
||||
'status_check' => 'active',
|
||||
'data' => 'active',
|
||||
'activations' => (string) ( $license['site_count'] ?? 1 ),
|
||||
'activations_limit' => (string) ( $license['license_limit'] ?? 'unlimited' ),
|
||||
'activations_remaining' => (string) ( $license['activations_left'] ?? 'unlimited' ),
|
||||
)
|
||||
);
|
||||
$body = wp_json_encode( [
|
||||
'success' => true,
|
||||
'status_check' => 'active',
|
||||
'data' => 'active',
|
||||
'activations' => (string) ( $license['site_count'] ?? 1 ),
|
||||
'activations_limit' => (string) ( $license['license_limit'] ?? 'unlimited' ),
|
||||
'activations_remaining'=> (string) ( $license['activations_left'] ?? 'unlimited' ),
|
||||
] );
|
||||
|
||||
return array(
|
||||
'response' => array(
|
||||
'code' => 200,
|
||||
'message' => 'OK',
|
||||
),
|
||||
return [
|
||||
'response' => [ 'code' => 200, 'message' => 'OK' ],
|
||||
'body' => $body,
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
);
|
||||
'headers' => [ 'content-type' => 'application/json' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -528,22 +493,17 @@ class LicenseProxy {
|
|||
* @return array
|
||||
*/
|
||||
private function format_generic_response( array $license ): array {
|
||||
$body = wp_json_encode(
|
||||
array(
|
||||
'success' => true,
|
||||
'license' => $license['status'] ?? 'valid',
|
||||
'expires' => $license['expires'] ?? 'lifetime',
|
||||
)
|
||||
);
|
||||
$body = wp_json_encode( [
|
||||
'success' => true,
|
||||
'license' => $license['status'] ?? 'valid',
|
||||
'expires' => $license['expires'] ?? 'lifetime',
|
||||
] );
|
||||
|
||||
return array(
|
||||
'response' => array(
|
||||
'code' => 200,
|
||||
'message' => 'OK',
|
||||
),
|
||||
return [
|
||||
'response' => [ 'code' => 200, 'message' => 'OK' ],
|
||||
'body' => $body,
|
||||
'headers' => array( 'content-type' => 'application/json' ),
|
||||
);
|
||||
'headers' => [ 'content-type' => 'application/json' ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -554,8 +514,8 @@ class LicenseProxy {
|
|||
* @return array
|
||||
*/
|
||||
private function get_plugin_response_config( string $plugin_slug, string $vendor ): array {
|
||||
$configs = $this->settings->get( 'plugin_response_configs', array() );
|
||||
return $configs[ $plugin_slug ][ $vendor ] ?? array();
|
||||
$configs = $this->settings->get( 'plugin_response_configs', [] );
|
||||
return $configs[ $plugin_slug ][ $vendor ] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ abstract class AbstractVendor implements VendorInterface {
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $config = array();
|
||||
protected array $config = [];
|
||||
|
||||
/**
|
||||
* 缓存前缀
|
||||
|
|
@ -50,7 +50,7 @@ abstract class AbstractVendor implements VendorInterface {
|
|||
*
|
||||
* @param array $config 配置
|
||||
*/
|
||||
public function __construct( array $config = array() ) {
|
||||
public function __construct( array $config = [] ) {
|
||||
$this->config = array_merge( $this->get_default_config(), $config );
|
||||
}
|
||||
|
||||
|
|
@ -60,13 +60,13 @@ abstract class AbstractVendor implements VendorInterface {
|
|||
* @return array
|
||||
*/
|
||||
protected function get_default_config(): array {
|
||||
return array(
|
||||
return [
|
||||
'api_url' => '',
|
||||
'api_key' => '',
|
||||
'api_secret' => '',
|
||||
'timeout' => 15,
|
||||
'enabled' => true,
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -94,13 +94,13 @@ abstract class AbstractVendor implements VendorInterface {
|
|||
* @param string $method 方法 (GET/POST)
|
||||
* @return array|null
|
||||
*/
|
||||
protected function api_request( string $endpoint, array $params = array(), string $method = 'GET' ): ?array {
|
||||
protected function api_request( string $endpoint, array $params = [], string $method = 'GET' ): ?array {
|
||||
$url = trailingslashit( $this->config['api_url'] ) . ltrim( $endpoint, '/' );
|
||||
|
||||
$args = array(
|
||||
$args = [
|
||||
'timeout' => $this->config['timeout'],
|
||||
'headers' => $this->get_request_headers(),
|
||||
);
|
||||
];
|
||||
|
||||
if ( $method === 'GET' && ! empty( $params ) ) {
|
||||
$url = add_query_arg( $params, $url );
|
||||
|
|
@ -113,14 +113,11 @@ abstract class AbstractVendor implements VendorInterface {
|
|||
: wp_remote_post( $url, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error(
|
||||
'Vendor API request failed',
|
||||
array(
|
||||
'vendor' => $this->get_id(),
|
||||
'endpoint' => $endpoint,
|
||||
'error' => $response->get_error_message(),
|
||||
)
|
||||
);
|
||||
Logger::error( 'Vendor API request failed', [
|
||||
'vendor' => $this->get_id(),
|
||||
'endpoint' => $endpoint,
|
||||
'error' => $response->get_error_message(),
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -128,27 +125,21 @@ abstract class AbstractVendor implements VendorInterface {
|
|||
$body = wp_remote_retrieve_body( $response );
|
||||
|
||||
if ( $code !== 200 ) {
|
||||
Logger::warning(
|
||||
'Vendor API non-200 response',
|
||||
array(
|
||||
'vendor' => $this->get_id(),
|
||||
'endpoint' => $endpoint,
|
||||
'code' => $code,
|
||||
)
|
||||
);
|
||||
Logger::warning( 'Vendor API non-200 response', [
|
||||
'vendor' => $this->get_id(),
|
||||
'endpoint' => $endpoint,
|
||||
'code' => $code,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
Logger::error(
|
||||
'Vendor API invalid JSON',
|
||||
array(
|
||||
'vendor' => $this->get_id(),
|
||||
'endpoint' => $endpoint,
|
||||
)
|
||||
);
|
||||
Logger::error( 'Vendor API invalid JSON', [
|
||||
'vendor' => $this->get_id(),
|
||||
'endpoint' => $endpoint,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -161,10 +152,10 @@ abstract class AbstractVendor implements VendorInterface {
|
|||
* @return array
|
||||
*/
|
||||
protected function get_request_headers(): array {
|
||||
return array(
|
||||
return [
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -213,7 +204,7 @@ abstract class AbstractVendor implements VendorInterface {
|
|||
public function search_plugins( string $keyword ): array {
|
||||
$all_plugins = $this->get_plugins( 1, 1000 );
|
||||
$keyword = strtolower( $keyword );
|
||||
$results = array();
|
||||
$results = [];
|
||||
|
||||
foreach ( $all_plugins['plugins'] as $plugin ) {
|
||||
$name = strtolower( $plugin['name'] ?? '' );
|
||||
|
|
@ -260,7 +251,7 @@ abstract class AbstractVendor implements VendorInterface {
|
|||
* @return array
|
||||
*/
|
||||
protected function normalize_plugin( array $raw_plugin ): array {
|
||||
return array(
|
||||
return [
|
||||
'slug' => $raw_plugin['slug'] ?? '',
|
||||
'name' => $raw_plugin['name'] ?? $raw_plugin['title'] ?? '',
|
||||
'version' => $raw_plugin['version'] ?? '',
|
||||
|
|
@ -273,6 +264,6 @@ abstract class AbstractVendor implements VendorInterface {
|
|||
'requires_php' => $raw_plugin['requires_php'] ?? '',
|
||||
'last_updated' => $raw_plugin['last_updated'] ?? $raw_plugin['modified'] ?? '',
|
||||
'vendor' => $this->get_id(),
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class VendorManager {
|
|||
*
|
||||
* @var VendorInterface[]
|
||||
*/
|
||||
private array $vendors = array();
|
||||
private array $vendors = [];
|
||||
|
||||
/**
|
||||
* 单例实例
|
||||
|
|
@ -73,7 +73,7 @@ class VendorManager {
|
|||
* 加载已配置的供应商
|
||||
*/
|
||||
private function load_vendors(): void {
|
||||
$vendor_configs = $this->settings->get( 'vendors', array() );
|
||||
$vendor_configs = $this->settings->get( 'vendors', [] );
|
||||
|
||||
foreach ( $vendor_configs as $vendor_id => $config ) {
|
||||
if ( empty( $config['enabled'] ) ) {
|
||||
|
|
@ -111,13 +111,10 @@ class VendorManager {
|
|||
// return new EDDVendor($vendor_id, $name, $config);
|
||||
|
||||
default:
|
||||
Logger::warning(
|
||||
'Unknown vendor type',
|
||||
array(
|
||||
'vendor_id' => $vendor_id,
|
||||
'type' => $type,
|
||||
)
|
||||
);
|
||||
Logger::warning( 'Unknown vendor type', [
|
||||
'vendor_id' => $vendor_id,
|
||||
'type' => $type,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -164,11 +161,11 @@ class VendorManager {
|
|||
* @return array
|
||||
*/
|
||||
public function get_vendors_info(): array {
|
||||
$info = array();
|
||||
$info = [];
|
||||
foreach ( $this->vendors as $vendor ) {
|
||||
$info[ $vendor->get_id() ] = array_merge(
|
||||
$vendor->get_info(),
|
||||
array( 'available' => $vendor->is_available() )
|
||||
[ 'available' => $vendor->is_available() ]
|
||||
);
|
||||
}
|
||||
return $info;
|
||||
|
|
@ -182,10 +179,10 @@ class VendorManager {
|
|||
* @return array
|
||||
*/
|
||||
public function search_plugins( string $keyword, string $vendor_id = '' ): array {
|
||||
$results = array();
|
||||
$results = [];
|
||||
|
||||
$vendors = ! empty( $vendor_id )
|
||||
? array( $this->get_vendor( $vendor_id ) )
|
||||
? [ $this->get_vendor( $vendor_id ) ]
|
||||
: $this->get_vendors( true );
|
||||
|
||||
foreach ( $vendors as $vendor ) {
|
||||
|
|
@ -201,13 +198,10 @@ class VendorManager {
|
|||
$results[] = $plugin;
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
Logger::error(
|
||||
'Vendor search failed',
|
||||
array(
|
||||
'vendor' => $vendor->get_id(),
|
||||
'error' => $e->getMessage(),
|
||||
)
|
||||
);
|
||||
Logger::error( 'Vendor search failed', [
|
||||
'vendor' => $vendor->get_id(),
|
||||
'error' => $e->getMessage(),
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +280,7 @@ class VendorManager {
|
|||
* @return bool
|
||||
*/
|
||||
public function add_vendor_config( string $vendor_id, array $config ): bool {
|
||||
$vendors = $this->settings->get( 'vendors', array() );
|
||||
$vendors = $this->settings->get( 'vendors', [] );
|
||||
$vendors[ $vendor_id ] = $config;
|
||||
|
||||
$result = $this->settings->set( 'vendors', $vendors );
|
||||
|
|
@ -309,7 +303,7 @@ class VendorManager {
|
|||
* @return bool
|
||||
*/
|
||||
public function remove_vendor_config( string $vendor_id ): bool {
|
||||
$vendors = $this->settings->get( 'vendors', array() );
|
||||
$vendors = $this->settings->get( 'vendors', [] );
|
||||
|
||||
if ( ! isset( $vendors[ $vendor_id ] ) ) {
|
||||
return false;
|
||||
|
|
@ -331,29 +325,29 @@ class VendorManager {
|
|||
$vendor = $this->get_vendor( $vendor_id );
|
||||
|
||||
if ( $vendor === null ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __( '供应商不存在', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
$available = $vendor->is_available();
|
||||
|
||||
if ( ! $available ) {
|
||||
return array(
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __( '供应商连接失败,请检查配置', 'wpbridge' ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
// 尝试获取插件列表
|
||||
$plugins = $vendor->get_plugins( 1, 10 );
|
||||
|
||||
return array(
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => __( '连接成功', 'wpbridge' ),
|
||||
'plugin_count' => $plugins['total'] ?? count( $plugins['plugins'] ),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -362,19 +356,19 @@ class VendorManager {
|
|||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
$total_vendors = count( $this->vendors );
|
||||
$active_vendors = count( $this->get_vendors( true ) );
|
||||
$total_plugins = 0;
|
||||
$total_vendors = count( $this->vendors );
|
||||
$active_vendors = count( $this->get_vendors( true ) );
|
||||
$total_plugins = 0;
|
||||
|
||||
foreach ( $this->get_vendors( true ) as $vendor ) {
|
||||
$plugins = $vendor->get_plugins( 1, 1 );
|
||||
$plugins = $vendor->get_plugins( 1, 1 );
|
||||
$total_plugins += $plugins['total'] ?? 0;
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'total_vendors' => $total_vendors,
|
||||
'active_vendors' => $active_vendors,
|
||||
'total_plugins' => $total_plugins,
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
* @param string $vendor_name 供应商名称
|
||||
* @param array $config 配置
|
||||
*/
|
||||
public function __construct( string $vendor_id, string $vendor_name, array $config = array() ) {
|
||||
public function __construct( string $vendor_id, string $vendor_name, array $config = [] ) {
|
||||
$this->vendor_id = $vendor_id;
|
||||
$this->vendor_name = $vendor_name;
|
||||
parent::__construct( $config );
|
||||
|
|
@ -63,17 +63,14 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
* @return array
|
||||
*/
|
||||
protected function get_default_config(): array {
|
||||
return array_merge(
|
||||
parent::get_default_config(),
|
||||
array(
|
||||
'api_version' => 'v2', // API 版本
|
||||
'product_id' => '', // 产品 ID(某些商店需要)
|
||||
'instance' => '', // 实例标识
|
||||
'use_rest_api' => true, // 是否使用 REST API
|
||||
'products_endpoint' => '/wp-json/wc/v3/products', // 产品列表端点
|
||||
'download_endpoint' => '/wp-json/wc-am/v2/download', // 下载端点
|
||||
)
|
||||
);
|
||||
return array_merge( parent::get_default_config(), [
|
||||
'api_version' => 'v2', // API 版本
|
||||
'product_id' => '', // 产品 ID(某些商店需要)
|
||||
'instance' => '', // 实例标识
|
||||
'use_rest_api' => true, // 是否使用 REST API
|
||||
'products_endpoint' => '/wp-json/wc/v3/products', // 产品列表端点
|
||||
'download_endpoint' => '/wp-json/wc-am/v2/download', // 下载端点
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,14 +88,14 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
* @return array
|
||||
*/
|
||||
public function get_info(): array {
|
||||
return array(
|
||||
return [
|
||||
'id' => $this->vendor_id,
|
||||
'name' => $this->vendor_name,
|
||||
'url' => $this->config['api_url'],
|
||||
'api_type' => 'wc_am',
|
||||
'api_version' => $this->config['api_version'],
|
||||
'requires_key' => true,
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -133,13 +130,10 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
}
|
||||
|
||||
// 尝试获取产品列表来验证
|
||||
$response = $this->api_request(
|
||||
$this->config['products_endpoint'],
|
||||
array(
|
||||
'per_page' => 1,
|
||||
'status' => 'publish',
|
||||
)
|
||||
);
|
||||
$response = $this->api_request( $this->config['products_endpoint'], [
|
||||
'per_page' => 1,
|
||||
'status' => 'publish',
|
||||
] );
|
||||
|
||||
$valid = $response !== null;
|
||||
$this->set_cache( $cache_key, $valid, 300 ); // 5分钟缓存
|
||||
|
|
@ -162,26 +156,23 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
return $cached;
|
||||
}
|
||||
|
||||
$response = $this->api_request(
|
||||
$this->config['products_endpoint'],
|
||||
array(
|
||||
'page' => $page,
|
||||
'per_page' => $limit,
|
||||
'status' => 'publish',
|
||||
'type' => 'simple', // 或 'variable' 取决于商店配置
|
||||
'category' => $this->config['category'] ?? '', // 可选:按分类过滤
|
||||
)
|
||||
);
|
||||
$response = $this->api_request( $this->config['products_endpoint'], [
|
||||
'page' => $page,
|
||||
'per_page' => $limit,
|
||||
'status' => 'publish',
|
||||
'type' => 'simple', // 或 'variable' 取决于商店配置
|
||||
'category' => $this->config['category'] ?? '', // 可选:按分类过滤
|
||||
] );
|
||||
|
||||
if ( $response === null ) {
|
||||
return array(
|
||||
'plugins' => array(),
|
||||
return [
|
||||
'plugins' => [],
|
||||
'total' => 0,
|
||||
'pages' => 0,
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
$plugins = array();
|
||||
$plugins = [];
|
||||
foreach ( $response as $product ) {
|
||||
$plugin = $this->normalize_wc_product( $product );
|
||||
if ( $plugin !== null ) {
|
||||
|
|
@ -189,11 +180,11 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
}
|
||||
}
|
||||
|
||||
$result = array(
|
||||
$result = [
|
||||
'plugins' => $plugins,
|
||||
'total' => count( $plugins ), // WC API 返回 X-WP-Total header
|
||||
'pages' => 1, // WC API 返回 X-WP-TotalPages header
|
||||
);
|
||||
];
|
||||
|
||||
$this->set_cache( $cache_key, $result );
|
||||
|
||||
|
|
@ -218,7 +209,7 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'slug' => $slug,
|
||||
'name' => $product['name'] ?? '',
|
||||
'version' => $this->extract_version( $product ),
|
||||
|
|
@ -233,7 +224,7 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
'price' => $product['price'] ?? '0',
|
||||
'product_id' => $product['id'] ?? 0,
|
||||
'vendor' => $this->get_id(),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -244,16 +235,16 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
*/
|
||||
protected function is_plugin_product( array $product ): bool {
|
||||
// 检查分类
|
||||
$categories = $product['categories'] ?? array();
|
||||
$categories = $product['categories'] ?? [];
|
||||
foreach ( $categories as $cat ) {
|
||||
$cat_slug = strtolower( $cat['slug'] ?? '' );
|
||||
if ( in_array( $cat_slug, array( 'plugins', 'wordpress-plugins', 'wp-plugins' ), true ) ) {
|
||||
if ( in_array( $cat_slug, [ 'plugins', 'wordpress-plugins', 'wp-plugins' ], true ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查标签
|
||||
$tags = $product['tags'] ?? array();
|
||||
$tags = $product['tags'] ?? [];
|
||||
foreach ( $tags as $tag ) {
|
||||
$tag_slug = strtolower( $tag['slug'] ?? '' );
|
||||
if ( strpos( $tag_slug, 'plugin' ) !== false ) {
|
||||
|
|
@ -262,7 +253,7 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
}
|
||||
|
||||
// 检查是否有下载文件
|
||||
$downloads = $product['downloads'] ?? array();
|
||||
$downloads = $product['downloads'] ?? [];
|
||||
foreach ( $downloads as $download ) {
|
||||
$file = strtolower( $download['file'] ?? '' );
|
||||
if ( strpos( $file, '.zip' ) !== false ) {
|
||||
|
|
@ -323,7 +314,7 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
}
|
||||
|
||||
// 从下载文件名提取
|
||||
$downloads = $product['downloads'] ?? array();
|
||||
$downloads = $product['downloads'] ?? [];
|
||||
foreach ( $downloads as $download ) {
|
||||
$file = $download['file'] ?? '';
|
||||
if ( preg_match( '/[\-_]v?(\d+\.\d+(?:\.\d+)?)/i', $file, $matches ) ) {
|
||||
|
|
@ -358,7 +349,7 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
* @return string
|
||||
*/
|
||||
protected function extract_meta( array $product, string $meta_key ): string {
|
||||
$meta_data = $product['meta_data'] ?? array();
|
||||
$meta_data = $product['meta_data'] ?? [];
|
||||
foreach ( $meta_data as $meta ) {
|
||||
if ( ( $meta['key'] ?? '' ) === $meta_key ) {
|
||||
return (string) ( $meta['value'] ?? '' );
|
||||
|
|
@ -391,14 +382,14 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
return null; // 无更新
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'version' => $latest_version,
|
||||
'download_url' => $this->get_download_url( $slug, $latest_version ),
|
||||
'changelog' => $this->get_changelog( $slug ),
|
||||
'tested' => $plugin['tested'] ?? '',
|
||||
'requires' => $plugin['requires'] ?? '',
|
||||
'requires_php' => $plugin['requires_php'] ?? '',
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -422,11 +413,11 @@ class WooCommerceVendor extends AbstractVendor {
|
|||
}
|
||||
|
||||
// 构建 WC API Manager 下载链接
|
||||
$params = array(
|
||||
$params = [
|
||||
'product_id' => $product_id,
|
||||
'api_key' => $this->config['api_key'],
|
||||
'instance' => $this->config['instance'] ?: $this->generate_instance_id(),
|
||||
);
|
||||
];
|
||||
|
||||
if ( ! empty( $version ) ) {
|
||||
$params['version'] = $version;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\Core;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,486 +17,486 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class BackupManager {
|
||||
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_backups';
|
||||
|
||||
/**
|
||||
* 备份目录名
|
||||
*/
|
||||
const BACKUP_DIR = 'wpbridge-backups';
|
||||
|
||||
/**
|
||||
* 最大保留备份数
|
||||
*/
|
||||
const MAX_BACKUPS = 5;
|
||||
|
||||
/**
|
||||
* 单例实例
|
||||
*
|
||||
* @var BackupManager|null
|
||||
*/
|
||||
private static ?BackupManager $instance = null;
|
||||
|
||||
/**
|
||||
* 备份记录缓存
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $backups = null;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*
|
||||
* @return BackupManager
|
||||
*/
|
||||
public static function get_instance(): BackupManager {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有构造函数
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 在更新前创建备份
|
||||
add_filter( 'upgrader_pre_install', array( $this, 'pre_install_backup' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份目录路径
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_backup_dir(): string {
|
||||
$upload_dir = wp_upload_dir();
|
||||
return trailingslashit( $upload_dir['basedir'] ) . self::BACKUP_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保备份目录存在
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function ensure_backup_dir(): bool {
|
||||
$dir = $this->get_backup_dir();
|
||||
|
||||
if ( ! file_exists( $dir ) ) {
|
||||
if ( ! wp_mkdir_p( $dir ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建 .htaccess 防止直接访问
|
||||
$htaccess = $dir . '/.htaccess';
|
||||
if ( ! file_exists( $htaccess ) ) {
|
||||
file_put_contents( $htaccess, "Deny from all\n" );
|
||||
}
|
||||
|
||||
// 创建 index.php
|
||||
$index = $dir . '/index.php';
|
||||
if ( ! file_exists( $index ) ) {
|
||||
file_put_contents( $index, "<?php\n// Silence is golden.\n" );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有备份记录
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->backups ) {
|
||||
$this->backups = get_option( self::OPTION_NAME, array() );
|
||||
if ( ! is_array( $this->backups ) ) {
|
||||
$this->backups = array();
|
||||
}
|
||||
}
|
||||
return $this->backups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目的备份列表
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_backups( string $item_key ): array {
|
||||
$backups = $this->get_all();
|
||||
return $backups[ $item_key ] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新前创建备份
|
||||
*
|
||||
* @param bool|WP_Error $response 响应
|
||||
* @param array $hook_extra 额外参数
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function pre_install_backup( $response, $hook_extra ) {
|
||||
// 检查是否启用了备份
|
||||
$settings = new Settings();
|
||||
if ( ! $settings->get( 'backup_enabled', true ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// 确定项目类型和路径
|
||||
if ( ! empty( $hook_extra['plugin'] ) ) {
|
||||
$item_key = 'plugin:' . $hook_extra['plugin'];
|
||||
$source_path = WP_PLUGIN_DIR . '/' . dirname( $hook_extra['plugin'] );
|
||||
|
||||
// 单文件插件
|
||||
if ( dirname( $hook_extra['plugin'] ) === '.' ) {
|
||||
$source_path = WP_PLUGIN_DIR . '/' . $hook_extra['plugin'];
|
||||
}
|
||||
} elseif ( ! empty( $hook_extra['theme'] ) ) {
|
||||
$item_key = 'theme:' . $hook_extra['theme'];
|
||||
$source_path = get_theme_root() . '/' . $hook_extra['theme'];
|
||||
} else {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// 创建备份
|
||||
$this->create_backup( $item_key, $source_path );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建备份
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $source_path 源路径
|
||||
* @return array|false 备份信息或失败
|
||||
*/
|
||||
public function create_backup( string $item_key, string $source_path ) {
|
||||
if ( ! file_exists( $source_path ) ) {
|
||||
Logger::warning( "Backup failed: source not found - {$source_path}" );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->ensure_backup_dir() ) {
|
||||
Logger::error( 'Backup failed: cannot create backup directory' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取当前版本
|
||||
$version = $this->get_item_version( $item_key, $source_path );
|
||||
|
||||
// 生成备份文件名
|
||||
$backup_id = wp_generate_uuid4();
|
||||
$backup_filename = sanitize_file_name( str_replace( ':', '-', $item_key ) ) . '-' . $version . '-' . gmdate( 'Ymd-His' ) . '.zip';
|
||||
$backup_path = $this->get_backup_dir() . '/' . $backup_filename;
|
||||
|
||||
// 创建 ZIP 备份
|
||||
if ( ! $this->create_zip( $source_path, $backup_path ) ) {
|
||||
Logger::error( "Backup failed: cannot create zip - {$backup_path}" );
|
||||
return false;
|
||||
}
|
||||
|
||||
// 记录备份信息
|
||||
$backup_info = array(
|
||||
'id' => $backup_id,
|
||||
'filename' => $backup_filename,
|
||||
'version' => $version,
|
||||
'size' => filesize( $backup_path ),
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
);
|
||||
|
||||
$this->add_backup_record( $item_key, $backup_info );
|
||||
|
||||
// 清理旧备份
|
||||
$this->cleanup_old_backups( $item_key );
|
||||
|
||||
Logger::info( "Backup created: {$item_key} v{$version}" );
|
||||
|
||||
return $backup_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 ZIP 文件
|
||||
*
|
||||
* @param string $source_path 源路径
|
||||
* @param string $zip_path ZIP 路径
|
||||
* @return bool
|
||||
*/
|
||||
private function create_zip( string $source_path, string $zip_path ): bool {
|
||||
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||
Logger::error( 'ZipArchive class not available' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ( $zip->open( $zip_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ) !== true ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( is_file( $source_path ) ) {
|
||||
// 单文件
|
||||
$zip->addFile( $source_path, basename( $source_path ) );
|
||||
} else {
|
||||
// 目录
|
||||
$this->add_dir_to_zip( $zip, $source_path, basename( $source_path ) );
|
||||
}
|
||||
|
||||
return $zip->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归添加目录到 ZIP
|
||||
*
|
||||
* @param \ZipArchive $zip ZIP 对象
|
||||
* @param string $dir 目录路径
|
||||
* @param string $zip_dir ZIP 内目录名
|
||||
*/
|
||||
private function add_dir_to_zip( \ZipArchive $zip, string $dir, string $zip_dir ): void {
|
||||
$files = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS ),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ( $files as $file ) {
|
||||
$file_path = $file->getRealPath();
|
||||
$relative_path = $zip_dir . '/' . substr( $file_path, strlen( $dir ) + 1 );
|
||||
|
||||
if ( $file->isDir() ) {
|
||||
$zip->addEmptyDir( $relative_path );
|
||||
} else {
|
||||
$zip->addFile( $file_path, $relative_path );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目版本
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $source_path 源路径
|
||||
* @return string
|
||||
*/
|
||||
private function get_item_version( string $item_key, string $source_path ): string {
|
||||
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
|
||||
$plugin_file = substr( $item_key, 7 );
|
||||
if ( ! function_exists( 'get_plugin_data' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
|
||||
$plugin_path = is_file( $source_path ) ? $source_path : $source_path . '/' . basename( $plugin_file );
|
||||
if ( file_exists( $plugin_path ) ) {
|
||||
$data = get_plugin_data( $plugin_path, false, false );
|
||||
return $data['Version'] ?? '0.0.0';
|
||||
}
|
||||
} elseif ( strpos( $item_key, 'theme:' ) === 0 ) {
|
||||
$theme_slug = substr( $item_key, 6 );
|
||||
$theme = wp_get_theme( $theme_slug );
|
||||
if ( $theme->exists() ) {
|
||||
return $theme->get( 'Version' );
|
||||
}
|
||||
}
|
||||
|
||||
return '0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加备份记录
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param array $backup_info 备份信息
|
||||
*/
|
||||
private function add_backup_record( string $item_key, array $backup_info ): void {
|
||||
$backups = $this->get_all();
|
||||
|
||||
if ( ! isset( $backups[ $item_key ] ) ) {
|
||||
$backups[ $item_key ] = array();
|
||||
}
|
||||
|
||||
array_unshift( $backups[ $item_key ], $backup_info );
|
||||
|
||||
$this->backups = $backups;
|
||||
update_option( self::OPTION_NAME, $backups );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧备份
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
*/
|
||||
private function cleanup_old_backups( string $item_key ): void {
|
||||
$backups = $this->get_all();
|
||||
|
||||
if ( ! isset( $backups[ $item_key ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$item_backups = $backups[ $item_key ];
|
||||
|
||||
while ( count( $item_backups ) > self::MAX_BACKUPS ) {
|
||||
$old_backup = array_pop( $item_backups );
|
||||
|
||||
// 删除文件
|
||||
$file_path = $this->get_backup_dir() . '/' . $old_backup['filename'];
|
||||
if ( file_exists( $file_path ) ) {
|
||||
unlink( $file_path );
|
||||
}
|
||||
}
|
||||
|
||||
$backups[ $item_key ] = $item_backups;
|
||||
$this->backups = $backups;
|
||||
update_option( self::OPTION_NAME, $backups );
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚到指定备份
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $backup_id 备份 ID
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function rollback( string $item_key, string $backup_id ) {
|
||||
$item_backups = $this->get_item_backups( $item_key );
|
||||
|
||||
// 查找备份
|
||||
$backup = null;
|
||||
foreach ( $item_backups as $b ) {
|
||||
if ( $b['id'] === $backup_id ) {
|
||||
$backup = $b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $backup ) {
|
||||
return new \WP_Error( 'backup_not_found', __( '备份不存在', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
$backup_path = $this->get_backup_dir() . '/' . $backup['filename'];
|
||||
|
||||
if ( ! file_exists( $backup_path ) ) {
|
||||
return new \WP_Error( 'backup_file_missing', __( '备份文件不存在', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
// 确定目标路径
|
||||
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
|
||||
$plugin_file = substr( $item_key, 7 );
|
||||
$target_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin_file );
|
||||
|
||||
if ( dirname( $plugin_file ) === '.' ) {
|
||||
// 单文件插件
|
||||
$target_dir = WP_PLUGIN_DIR;
|
||||
}
|
||||
} elseif ( strpos( $item_key, 'theme:' ) === 0 ) {
|
||||
$theme_slug = substr( $item_key, 6 );
|
||||
$target_dir = get_theme_root() . '/' . $theme_slug;
|
||||
} else {
|
||||
return new \WP_Error( 'invalid_item', __( '无效的项目', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
// 解压备份
|
||||
$result = $this->extract_zip( $backup_path, dirname( $target_dir ) );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
Logger::info( "Rollback completed: {$item_key} to v{$backup['version']}" );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压 ZIP 文件
|
||||
*
|
||||
* @param string $zip_path ZIP 路径
|
||||
* @param string $target_dir 目标目录
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private function extract_zip( string $zip_path, string $target_dir ) {
|
||||
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||
return new \WP_Error( 'no_zip', __( 'ZipArchive 不可用', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ( $zip->open( $zip_path ) !== true ) {
|
||||
return new \WP_Error( 'zip_open_failed', __( '无法打开备份文件', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
$zip->extractTo( $target_dir );
|
||||
$zip->close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除备份
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $backup_id 备份 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_backup( string $item_key, string $backup_id ): bool {
|
||||
$backups = $this->get_all();
|
||||
|
||||
if ( ! isset( $backups[ $item_key ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( $backups[ $item_key ] as $index => $backup ) {
|
||||
if ( $backup['id'] === $backup_id ) {
|
||||
// 删除文件
|
||||
$file_path = $this->get_backup_dir() . '/' . $backup['filename'];
|
||||
if ( file_exists( $file_path ) ) {
|
||||
unlink( $file_path );
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
unset( $backups[ $item_key ][ $index ] );
|
||||
$backups[ $item_key ] = array_values( $backups[ $item_key ] );
|
||||
|
||||
$this->backups = $backups;
|
||||
update_option( self::OPTION_NAME, $backups );
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份总大小
|
||||
*
|
||||
* @return int 字节数
|
||||
*/
|
||||
public function get_total_size(): int {
|
||||
$total = 0;
|
||||
$backups = $this->get_all();
|
||||
|
||||
foreach ( $backups as $item_backups ) {
|
||||
foreach ( $item_backups as $backup ) {
|
||||
$total += $backup['size'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->backups = null;
|
||||
}
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_backups';
|
||||
|
||||
/**
|
||||
* 备份目录名
|
||||
*/
|
||||
const BACKUP_DIR = 'wpbridge-backups';
|
||||
|
||||
/**
|
||||
* 最大保留备份数
|
||||
*/
|
||||
const MAX_BACKUPS = 5;
|
||||
|
||||
/**
|
||||
* 单例实例
|
||||
*
|
||||
* @var BackupManager|null
|
||||
*/
|
||||
private static ?BackupManager $instance = null;
|
||||
|
||||
/**
|
||||
* 备份记录缓存
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $backups = null;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*
|
||||
* @return BackupManager
|
||||
*/
|
||||
public static function get_instance(): BackupManager {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有构造函数
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 在更新前创建备份
|
||||
add_filter( 'upgrader_pre_install', [ $this, 'pre_install_backup' ], 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份目录路径
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_backup_dir(): string {
|
||||
$upload_dir = wp_upload_dir();
|
||||
return trailingslashit( $upload_dir['basedir'] ) . self::BACKUP_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保备份目录存在
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function ensure_backup_dir(): bool {
|
||||
$dir = $this->get_backup_dir();
|
||||
|
||||
if ( ! file_exists( $dir ) ) {
|
||||
if ( ! wp_mkdir_p( $dir ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建 .htaccess 防止直接访问
|
||||
$htaccess = $dir . '/.htaccess';
|
||||
if ( ! file_exists( $htaccess ) ) {
|
||||
file_put_contents( $htaccess, "Deny from all\n" );
|
||||
}
|
||||
|
||||
// 创建 index.php
|
||||
$index = $dir . '/index.php';
|
||||
if ( ! file_exists( $index ) ) {
|
||||
file_put_contents( $index, "<?php\n// Silence is golden.\n" );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有备份记录
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->backups ) {
|
||||
$this->backups = get_option( self::OPTION_NAME, [] );
|
||||
if ( ! is_array( $this->backups ) ) {
|
||||
$this->backups = [];
|
||||
}
|
||||
}
|
||||
return $this->backups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目的备份列表
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_backups( string $item_key ): array {
|
||||
$backups = $this->get_all();
|
||||
return $backups[ $item_key ] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新前创建备份
|
||||
*
|
||||
* @param bool|WP_Error $response 响应
|
||||
* @param array $hook_extra 额外参数
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function pre_install_backup( $response, $hook_extra ) {
|
||||
// 检查是否启用了备份
|
||||
$settings = new Settings();
|
||||
if ( ! $settings->get( 'backup_enabled', true ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// 确定项目类型和路径
|
||||
if ( ! empty( $hook_extra['plugin'] ) ) {
|
||||
$item_key = 'plugin:' . $hook_extra['plugin'];
|
||||
$source_path = WP_PLUGIN_DIR . '/' . dirname( $hook_extra['plugin'] );
|
||||
|
||||
// 单文件插件
|
||||
if ( dirname( $hook_extra['plugin'] ) === '.' ) {
|
||||
$source_path = WP_PLUGIN_DIR . '/' . $hook_extra['plugin'];
|
||||
}
|
||||
} elseif ( ! empty( $hook_extra['theme'] ) ) {
|
||||
$item_key = 'theme:' . $hook_extra['theme'];
|
||||
$source_path = get_theme_root() . '/' . $hook_extra['theme'];
|
||||
} else {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// 创建备份
|
||||
$this->create_backup( $item_key, $source_path );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建备份
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $source_path 源路径
|
||||
* @return array|false 备份信息或失败
|
||||
*/
|
||||
public function create_backup( string $item_key, string $source_path ) {
|
||||
if ( ! file_exists( $source_path ) ) {
|
||||
Logger::warning( "Backup failed: source not found - {$source_path}" );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->ensure_backup_dir() ) {
|
||||
Logger::error( 'Backup failed: cannot create backup directory' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取当前版本
|
||||
$version = $this->get_item_version( $item_key, $source_path );
|
||||
|
||||
// 生成备份文件名
|
||||
$backup_id = wp_generate_uuid4();
|
||||
$backup_filename = sanitize_file_name( str_replace( ':', '-', $item_key ) ) . '-' . $version . '-' . gmdate( 'Ymd-His' ) . '.zip';
|
||||
$backup_path = $this->get_backup_dir() . '/' . $backup_filename;
|
||||
|
||||
// 创建 ZIP 备份
|
||||
if ( ! $this->create_zip( $source_path, $backup_path ) ) {
|
||||
Logger::error( "Backup failed: cannot create zip - {$backup_path}" );
|
||||
return false;
|
||||
}
|
||||
|
||||
// 记录备份信息
|
||||
$backup_info = [
|
||||
'id' => $backup_id,
|
||||
'filename' => $backup_filename,
|
||||
'version' => $version,
|
||||
'size' => filesize( $backup_path ),
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
];
|
||||
|
||||
$this->add_backup_record( $item_key, $backup_info );
|
||||
|
||||
// 清理旧备份
|
||||
$this->cleanup_old_backups( $item_key );
|
||||
|
||||
Logger::info( "Backup created: {$item_key} v{$version}" );
|
||||
|
||||
return $backup_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 ZIP 文件
|
||||
*
|
||||
* @param string $source_path 源路径
|
||||
* @param string $zip_path ZIP 路径
|
||||
* @return bool
|
||||
*/
|
||||
private function create_zip( string $source_path, string $zip_path ): bool {
|
||||
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||
Logger::error( 'ZipArchive class not available' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ( $zip->open( $zip_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ) !== true ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( is_file( $source_path ) ) {
|
||||
// 单文件
|
||||
$zip->addFile( $source_path, basename( $source_path ) );
|
||||
} else {
|
||||
// 目录
|
||||
$this->add_dir_to_zip( $zip, $source_path, basename( $source_path ) );
|
||||
}
|
||||
|
||||
return $zip->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归添加目录到 ZIP
|
||||
*
|
||||
* @param \ZipArchive $zip ZIP 对象
|
||||
* @param string $dir 目录路径
|
||||
* @param string $zip_dir ZIP 内目录名
|
||||
*/
|
||||
private function add_dir_to_zip( \ZipArchive $zip, string $dir, string $zip_dir ): void {
|
||||
$files = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS ),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ( $files as $file ) {
|
||||
$file_path = $file->getRealPath();
|
||||
$relative_path = $zip_dir . '/' . substr( $file_path, strlen( $dir ) + 1 );
|
||||
|
||||
if ( $file->isDir() ) {
|
||||
$zip->addEmptyDir( $relative_path );
|
||||
} else {
|
||||
$zip->addFile( $file_path, $relative_path );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目版本
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $source_path 源路径
|
||||
* @return string
|
||||
*/
|
||||
private function get_item_version( string $item_key, string $source_path ): string {
|
||||
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
|
||||
$plugin_file = substr( $item_key, 7 );
|
||||
if ( ! function_exists( 'get_plugin_data' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
|
||||
$plugin_path = is_file( $source_path ) ? $source_path : $source_path . '/' . basename( $plugin_file );
|
||||
if ( file_exists( $plugin_path ) ) {
|
||||
$data = get_plugin_data( $plugin_path, false, false );
|
||||
return $data['Version'] ?? '0.0.0';
|
||||
}
|
||||
} elseif ( strpos( $item_key, 'theme:' ) === 0 ) {
|
||||
$theme_slug = substr( $item_key, 6 );
|
||||
$theme = wp_get_theme( $theme_slug );
|
||||
if ( $theme->exists() ) {
|
||||
return $theme->get( 'Version' );
|
||||
}
|
||||
}
|
||||
|
||||
return '0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加备份记录
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param array $backup_info 备份信息
|
||||
*/
|
||||
private function add_backup_record( string $item_key, array $backup_info ): void {
|
||||
$backups = $this->get_all();
|
||||
|
||||
if ( ! isset( $backups[ $item_key ] ) ) {
|
||||
$backups[ $item_key ] = [];
|
||||
}
|
||||
|
||||
array_unshift( $backups[ $item_key ], $backup_info );
|
||||
|
||||
$this->backups = $backups;
|
||||
update_option( self::OPTION_NAME, $backups );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧备份
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
*/
|
||||
private function cleanup_old_backups( string $item_key ): void {
|
||||
$backups = $this->get_all();
|
||||
|
||||
if ( ! isset( $backups[ $item_key ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$item_backups = $backups[ $item_key ];
|
||||
|
||||
while ( count( $item_backups ) > self::MAX_BACKUPS ) {
|
||||
$old_backup = array_pop( $item_backups );
|
||||
|
||||
// 删除文件
|
||||
$file_path = $this->get_backup_dir() . '/' . $old_backup['filename'];
|
||||
if ( file_exists( $file_path ) ) {
|
||||
unlink( $file_path );
|
||||
}
|
||||
}
|
||||
|
||||
$backups[ $item_key ] = $item_backups;
|
||||
$this->backups = $backups;
|
||||
update_option( self::OPTION_NAME, $backups );
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚到指定备份
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $backup_id 备份 ID
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function rollback( string $item_key, string $backup_id ) {
|
||||
$item_backups = $this->get_item_backups( $item_key );
|
||||
|
||||
// 查找备份
|
||||
$backup = null;
|
||||
foreach ( $item_backups as $b ) {
|
||||
if ( $b['id'] === $backup_id ) {
|
||||
$backup = $b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $backup ) {
|
||||
return new \WP_Error( 'backup_not_found', __( '备份不存在', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
$backup_path = $this->get_backup_dir() . '/' . $backup['filename'];
|
||||
|
||||
if ( ! file_exists( $backup_path ) ) {
|
||||
return new \WP_Error( 'backup_file_missing', __( '备份文件不存在', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
// 确定目标路径
|
||||
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
|
||||
$plugin_file = substr( $item_key, 7 );
|
||||
$target_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin_file );
|
||||
|
||||
if ( dirname( $plugin_file ) === '.' ) {
|
||||
// 单文件插件
|
||||
$target_dir = WP_PLUGIN_DIR;
|
||||
}
|
||||
} elseif ( strpos( $item_key, 'theme:' ) === 0 ) {
|
||||
$theme_slug = substr( $item_key, 6 );
|
||||
$target_dir = get_theme_root() . '/' . $theme_slug;
|
||||
} else {
|
||||
return new \WP_Error( 'invalid_item', __( '无效的项目', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
// 解压备份
|
||||
$result = $this->extract_zip( $backup_path, dirname( $target_dir ) );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
Logger::info( "Rollback completed: {$item_key} to v{$backup['version']}" );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压 ZIP 文件
|
||||
*
|
||||
* @param string $zip_path ZIP 路径
|
||||
* @param string $target_dir 目标目录
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private function extract_zip( string $zip_path, string $target_dir ) {
|
||||
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||
return new \WP_Error( 'no_zip', __( 'ZipArchive 不可用', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ( $zip->open( $zip_path ) !== true ) {
|
||||
return new \WP_Error( 'zip_open_failed', __( '无法打开备份文件', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
$zip->extractTo( $target_dir );
|
||||
$zip->close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除备份
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $backup_id 备份 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_backup( string $item_key, string $backup_id ): bool {
|
||||
$backups = $this->get_all();
|
||||
|
||||
if ( ! isset( $backups[ $item_key ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( $backups[ $item_key ] as $index => $backup ) {
|
||||
if ( $backup['id'] === $backup_id ) {
|
||||
// 删除文件
|
||||
$file_path = $this->get_backup_dir() . '/' . $backup['filename'];
|
||||
if ( file_exists( $file_path ) ) {
|
||||
unlink( $file_path );
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
unset( $backups[ $item_key ][ $index ] );
|
||||
$backups[ $item_key ] = array_values( $backups[ $item_key ] );
|
||||
|
||||
$this->backups = $backups;
|
||||
update_option( self::OPTION_NAME, $backups );
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份总大小
|
||||
*
|
||||
* @return int 字节数
|
||||
*/
|
||||
public function get_total_size(): int {
|
||||
$total = 0;
|
||||
$backups = $this->get_all();
|
||||
|
||||
foreach ( $backups as $item_backups ) {
|
||||
foreach ( $item_backups as $backup ) {
|
||||
$total += $backup['size'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->backups = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,7 @@ namespace WPBridge\Core;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,335 +17,335 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class ConfigManager {
|
||||
|
||||
/**
|
||||
* 配置版本
|
||||
*/
|
||||
const CONFIG_VERSION = '1.0';
|
||||
/**
|
||||
* 配置版本
|
||||
*/
|
||||
const CONFIG_VERSION = '1.0';
|
||||
|
||||
/**
|
||||
* 需要导出的选项
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $export_options = array(
|
||||
'wpbridge_sources',
|
||||
'wpbridge_settings',
|
||||
'wpbridge_ai_settings',
|
||||
'wpbridge_source_groups',
|
||||
'wpbridge_item_sources',
|
||||
'wpbridge_defaults',
|
||||
'wpbridge_source_registry',
|
||||
'wpbridge_plugin_types',
|
||||
);
|
||||
/**
|
||||
* 需要导出的选项
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $export_options = [
|
||||
'wpbridge_sources',
|
||||
'wpbridge_settings',
|
||||
'wpbridge_ai_settings',
|
||||
'wpbridge_source_groups',
|
||||
'wpbridge_item_sources',
|
||||
'wpbridge_defaults',
|
||||
'wpbridge_source_registry',
|
||||
'wpbridge_plugin_types',
|
||||
];
|
||||
|
||||
/**
|
||||
* 导出配置
|
||||
*
|
||||
* @param bool $include_secrets 是否包含敏感信息(API Key 等)
|
||||
* @return array
|
||||
*/
|
||||
public function export( bool $include_secrets = false ): array {
|
||||
$config = array(
|
||||
'version' => self::CONFIG_VERSION,
|
||||
'plugin' => WPBRIDGE_VERSION,
|
||||
'site_url' => get_site_url(),
|
||||
'exported' => current_time( 'mysql' ),
|
||||
'options' => array(),
|
||||
);
|
||||
/**
|
||||
* 导出配置
|
||||
*
|
||||
* @param bool $include_secrets 是否包含敏感信息(API Key 等)
|
||||
* @return array
|
||||
*/
|
||||
public function export( bool $include_secrets = false ): array {
|
||||
$config = [
|
||||
'version' => self::CONFIG_VERSION,
|
||||
'plugin' => WPBRIDGE_VERSION,
|
||||
'site_url' => get_site_url(),
|
||||
'exported' => current_time( 'mysql' ),
|
||||
'options' => [],
|
||||
];
|
||||
|
||||
foreach ( $this->export_options as $option_name ) {
|
||||
$value = get_option( $option_name, null );
|
||||
foreach ( $this->export_options as $option_name ) {
|
||||
$value = get_option( $option_name, null );
|
||||
|
||||
if ( null !== $value ) {
|
||||
// 处理敏感信息
|
||||
if ( ! $include_secrets ) {
|
||||
$value = $this->sanitize_secrets( $option_name, $value );
|
||||
}
|
||||
$config['options'][ $option_name ] = $value;
|
||||
}
|
||||
}
|
||||
if ( null !== $value ) {
|
||||
// 处理敏感信息
|
||||
if ( ! $include_secrets ) {
|
||||
$value = $this->sanitize_secrets( $option_name, $value );
|
||||
}
|
||||
$config['options'][ $option_name ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出为 JSON 字符串
|
||||
*
|
||||
* @param bool $include_secrets 是否包含敏感信息
|
||||
* @return string
|
||||
*/
|
||||
public function export_json( bool $include_secrets = false ): string {
|
||||
return wp_json_encode( $this->export( $include_secrets ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
|
||||
}
|
||||
/**
|
||||
* 导出为 JSON 字符串
|
||||
*
|
||||
* @param bool $include_secrets 是否包含敏感信息
|
||||
* @return string
|
||||
*/
|
||||
public function export_json( bool $include_secrets = false ): string {
|
||||
return wp_json_encode( $this->export( $include_secrets ), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入配置
|
||||
*
|
||||
* @param array $config 配置数据
|
||||
* @param bool $merge 是否合并(true=合并,false=覆盖)
|
||||
* @return array 导入结果
|
||||
*/
|
||||
public function import( array $config, bool $merge = true ): array {
|
||||
$result = array(
|
||||
'success' => true,
|
||||
'imported' => array(),
|
||||
'skipped' => array(),
|
||||
'errors' => array(),
|
||||
);
|
||||
/**
|
||||
* 导入配置
|
||||
*
|
||||
* @param array $config 配置数据
|
||||
* @param bool $merge 是否合并(true=合并,false=覆盖)
|
||||
* @return array 导入结果
|
||||
*/
|
||||
public function import( array $config, bool $merge = true ): array {
|
||||
$result = [
|
||||
'success' => true,
|
||||
'imported' => [],
|
||||
'skipped' => [],
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
// 验证配置格式
|
||||
$validation = $this->validate_config( $config );
|
||||
if ( ! $validation['valid'] ) {
|
||||
$result['success'] = false;
|
||||
$result['errors'] = $validation['errors'];
|
||||
return $result;
|
||||
}
|
||||
// 验证配置格式
|
||||
$validation = $this->validate_config( $config );
|
||||
if ( ! $validation['valid'] ) {
|
||||
$result['success'] = false;
|
||||
$result['errors'] = $validation['errors'];
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 导入选项
|
||||
foreach ( $config['options'] as $option_name => $value ) {
|
||||
// 只导入允许的选项
|
||||
if ( ! in_array( $option_name, $this->export_options, true ) ) {
|
||||
$result['skipped'][] = $option_name;
|
||||
continue;
|
||||
}
|
||||
// 导入选项
|
||||
foreach ( $config['options'] as $option_name => $value ) {
|
||||
// 只导入允许的选项
|
||||
if ( ! in_array( $option_name, $this->export_options, true ) ) {
|
||||
$result['skipped'][] = $option_name;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if ( $merge ) {
|
||||
$value = $this->merge_option( $option_name, $value );
|
||||
}
|
||||
try {
|
||||
if ( $merge ) {
|
||||
$value = $this->merge_option( $option_name, $value );
|
||||
}
|
||||
|
||||
if ( update_option( $option_name, $value ) ) {
|
||||
$result['imported'][] = $option_name;
|
||||
} else {
|
||||
// 值相同时 update_option 返回 false
|
||||
$result['imported'][] = $option_name;
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
$result['errors'][] = sprintf(
|
||||
__( '导入 %1$s 失败: %2$s', 'wpbridge' ),
|
||||
$option_name,
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
if ( update_option( $option_name, $value ) ) {
|
||||
$result['imported'][] = $option_name;
|
||||
} else {
|
||||
// 值相同时 update_option 返回 false
|
||||
$result['imported'][] = $option_name;
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
$result['errors'][] = sprintf(
|
||||
__( '导入 %s 失败: %s', 'wpbridge' ),
|
||||
$option_name,
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 字符串导入
|
||||
*
|
||||
* @param string $json JSON 字符串
|
||||
* @param bool $merge 是否合并
|
||||
* @return array 导入结果
|
||||
*/
|
||||
public function import_json( string $json, bool $merge = true ): array {
|
||||
$config = json_decode( $json, true );
|
||||
/**
|
||||
* 从 JSON 字符串导入
|
||||
*
|
||||
* @param string $json JSON 字符串
|
||||
* @param bool $merge 是否合并
|
||||
* @return array 导入结果
|
||||
*/
|
||||
public function import_json( string $json, bool $merge = true ): array {
|
||||
$config = json_decode( $json, true );
|
||||
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'errors' => array( __( 'JSON 格式无效', 'wpbridge' ) ),
|
||||
);
|
||||
}
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
return [
|
||||
'success' => false,
|
||||
'errors' => [ __( 'JSON 格式无效', 'wpbridge' ) ],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->import( $config, $merge );
|
||||
}
|
||||
return $this->import( $config, $merge );
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置格式
|
||||
*
|
||||
* @param array $config 配置数据
|
||||
* @return array
|
||||
*/
|
||||
public function validate_config( array $config ): array {
|
||||
$errors = array();
|
||||
/**
|
||||
* 验证配置格式
|
||||
*
|
||||
* @param array $config 配置数据
|
||||
* @return array
|
||||
*/
|
||||
public function validate_config( array $config ): array {
|
||||
$errors = [];
|
||||
|
||||
if ( empty( $config['version'] ) ) {
|
||||
$errors[] = __( '缺少配置版本号', 'wpbridge' );
|
||||
}
|
||||
if ( empty( $config['version'] ) ) {
|
||||
$errors[] = __( '缺少配置版本号', 'wpbridge' );
|
||||
}
|
||||
|
||||
if ( empty( $config['options'] ) || ! is_array( $config['options'] ) ) {
|
||||
$errors[] = __( '缺少配置选项', 'wpbridge' );
|
||||
}
|
||||
if ( empty( $config['options'] ) || ! is_array( $config['options'] ) ) {
|
||||
$errors[] = __( '缺少配置选项', 'wpbridge' );
|
||||
}
|
||||
|
||||
// 检查版本兼容性
|
||||
if ( ! empty( $config['version'] ) && version_compare( $config['version'], self::CONFIG_VERSION, '>' ) ) {
|
||||
$errors[] = sprintf(
|
||||
__( '配置版本 %1$s 高于当前支持的版本 %2$s', 'wpbridge' ),
|
||||
$config['version'],
|
||||
self::CONFIG_VERSION
|
||||
);
|
||||
}
|
||||
// 检查版本兼容性
|
||||
if ( ! empty( $config['version'] ) && version_compare( $config['version'], self::CONFIG_VERSION, '>' ) ) {
|
||||
$errors[] = sprintf(
|
||||
__( '配置版本 %s 高于当前支持的版本 %s', 'wpbridge' ),
|
||||
$config['version'],
|
||||
self::CONFIG_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'valid' => empty( $errors ),
|
||||
'errors' => $errors,
|
||||
);
|
||||
}
|
||||
return [
|
||||
'valid' => empty( $errors ),
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理敏感信息
|
||||
*
|
||||
* @param string $option_name 选项名
|
||||
* @param mixed $value 选项值
|
||||
* @return mixed
|
||||
*/
|
||||
private function sanitize_secrets( string $option_name, $value ) {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return $value;
|
||||
}
|
||||
/**
|
||||
* 清理敏感信息
|
||||
*
|
||||
* @param string $option_name 选项名
|
||||
* @param mixed $value 选项值
|
||||
* @return mixed
|
||||
*/
|
||||
private function sanitize_secrets( string $option_name, $value ) {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// 更新源中的敏感字段
|
||||
if ( 'wpbridge_sources' === $option_name ) {
|
||||
foreach ( $value as &$source ) {
|
||||
if ( isset( $source['auth_token'] ) && ! empty( $source['auth_token'] ) ) {
|
||||
$source['auth_token'] = '***REDACTED***';
|
||||
}
|
||||
if ( isset( $source['api_key'] ) && ! empty( $source['api_key'] ) ) {
|
||||
$source['api_key'] = '***REDACTED***';
|
||||
}
|
||||
}
|
||||
}
|
||||
// 更新源中的敏感字段
|
||||
if ( 'wpbridge_sources' === $option_name ) {
|
||||
foreach ( $value as &$source ) {
|
||||
if ( isset( $source['auth_token'] ) && ! empty( $source['auth_token'] ) ) {
|
||||
$source['auth_token'] = '***REDACTED***';
|
||||
}
|
||||
if ( isset( $source['api_key'] ) && ! empty( $source['api_key'] ) ) {
|
||||
$source['api_key'] = '***REDACTED***';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AI 设置中的敏感字段
|
||||
if ( 'wpbridge_ai_settings' === $option_name ) {
|
||||
if ( isset( $value['api_key'] ) && ! empty( $value['api_key'] ) ) {
|
||||
$value['api_key'] = '***REDACTED***';
|
||||
}
|
||||
}
|
||||
// AI 设置中的敏感字段
|
||||
if ( 'wpbridge_ai_settings' === $option_name ) {
|
||||
if ( isset( $value['api_key'] ) && ! empty( $value['api_key'] ) ) {
|
||||
$value['api_key'] = '***REDACTED***';
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并选项值
|
||||
*
|
||||
* @param string $option_name 选项名
|
||||
* @param mixed $new_value 新值
|
||||
* @return mixed
|
||||
*/
|
||||
private function merge_option( string $option_name, $new_value ) {
|
||||
$current = get_option( $option_name, array() );
|
||||
/**
|
||||
* 合并选项值
|
||||
*
|
||||
* @param string $option_name 选项名
|
||||
* @param mixed $new_value 新值
|
||||
* @return mixed
|
||||
*/
|
||||
private function merge_option( string $option_name, $new_value ) {
|
||||
$current = get_option( $option_name, [] );
|
||||
|
||||
// 如果当前值为空,直接使用新值
|
||||
if ( empty( $current ) ) {
|
||||
return $new_value;
|
||||
}
|
||||
// 如果当前值为空,直接使用新值
|
||||
if ( empty( $current ) ) {
|
||||
return $new_value;
|
||||
}
|
||||
|
||||
// 如果不是数组,直接覆盖
|
||||
if ( ! is_array( $current ) || ! is_array( $new_value ) ) {
|
||||
return $new_value;
|
||||
}
|
||||
// 如果不是数组,直接覆盖
|
||||
if ( ! is_array( $current ) || ! is_array( $new_value ) ) {
|
||||
return $new_value;
|
||||
}
|
||||
|
||||
// 更新源:按 ID 合并
|
||||
if ( 'wpbridge_sources' === $option_name ) {
|
||||
return $this->merge_sources( $current, $new_value );
|
||||
}
|
||||
// 更新源:按 ID 合并
|
||||
if ( 'wpbridge_sources' === $option_name ) {
|
||||
return $this->merge_sources( $current, $new_value );
|
||||
}
|
||||
|
||||
// 源分组:按 ID 合并
|
||||
if ( 'wpbridge_source_groups' === $option_name ) {
|
||||
return $this->merge_by_id( $current, $new_value );
|
||||
}
|
||||
// 源分组:按 ID 合并
|
||||
if ( 'wpbridge_source_groups' === $option_name ) {
|
||||
return $this->merge_by_id( $current, $new_value );
|
||||
}
|
||||
|
||||
// 其他数组:深度合并
|
||||
return array_replace_recursive( $current, $new_value );
|
||||
}
|
||||
// 其他数组:深度合并
|
||||
return array_replace_recursive( $current, $new_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并更新源
|
||||
*
|
||||
* @param array $current 当前源
|
||||
* @param array $new 新源
|
||||
* @return array
|
||||
*/
|
||||
private function merge_sources( array $current, array $new ): array {
|
||||
$merged = $current;
|
||||
$ids = array_column( $current, 'id' );
|
||||
/**
|
||||
* 合并更新源
|
||||
*
|
||||
* @param array $current 当前源
|
||||
* @param array $new 新源
|
||||
* @return array
|
||||
*/
|
||||
private function merge_sources( array $current, array $new ): array {
|
||||
$merged = $current;
|
||||
$ids = array_column( $current, 'id' );
|
||||
|
||||
foreach ( $new as $source ) {
|
||||
if ( empty( $source['id'] ) ) {
|
||||
continue;
|
||||
}
|
||||
foreach ( $new as $source ) {
|
||||
if ( empty( $source['id'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$index = array_search( $source['id'], $ids, true );
|
||||
$index = array_search( $source['id'], $ids, true );
|
||||
|
||||
if ( false !== $index ) {
|
||||
// 更新现有源(保留敏感信息)
|
||||
if ( isset( $source['auth_token'] ) && '***REDACTED***' === $source['auth_token'] ) {
|
||||
$source['auth_token'] = $merged[ $index ]['auth_token'] ?? '';
|
||||
}
|
||||
$merged[ $index ] = array_merge( $merged[ $index ], $source );
|
||||
} else {
|
||||
// 添加新源
|
||||
$merged[] = $source;
|
||||
}
|
||||
}
|
||||
if ( false !== $index ) {
|
||||
// 更新现有源(保留敏感信息)
|
||||
if ( isset( $source['auth_token'] ) && '***REDACTED***' === $source['auth_token'] ) {
|
||||
$source['auth_token'] = $merged[ $index ]['auth_token'] ?? '';
|
||||
}
|
||||
$merged[ $index ] = array_merge( $merged[ $index ], $source );
|
||||
} else {
|
||||
// 添加新源
|
||||
$merged[] = $source;
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 ID 合并数组
|
||||
*
|
||||
* @param array $current 当前数组
|
||||
* @param array $new 新数组
|
||||
* @return array
|
||||
*/
|
||||
private function merge_by_id( array $current, array $new ): array {
|
||||
$merged = $current;
|
||||
$ids = array_column( $current, 'id' );
|
||||
/**
|
||||
* 按 ID 合并数组
|
||||
*
|
||||
* @param array $current 当前数组
|
||||
* @param array $new 新数组
|
||||
* @return array
|
||||
*/
|
||||
private function merge_by_id( array $current, array $new ): array {
|
||||
$merged = $current;
|
||||
$ids = array_column( $current, 'id' );
|
||||
|
||||
foreach ( $new as $item ) {
|
||||
if ( empty( $item['id'] ) ) {
|
||||
continue;
|
||||
}
|
||||
foreach ( $new as $item ) {
|
||||
if ( empty( $item['id'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$index = array_search( $item['id'], $ids, true );
|
||||
$index = array_search( $item['id'], $ids, true );
|
||||
|
||||
if ( false !== $index ) {
|
||||
$merged[ $index ] = array_merge( $merged[ $index ], $item );
|
||||
} else {
|
||||
$merged[] = $item;
|
||||
}
|
||||
}
|
||||
if ( false !== $index ) {
|
||||
$merged[ $index ] = array_merge( $merged[ $index ], $item );
|
||||
} else {
|
||||
$merged[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建备份
|
||||
*
|
||||
* @return array 备份数据
|
||||
*/
|
||||
public function create_backup(): array {
|
||||
return $this->export( true );
|
||||
}
|
||||
/**
|
||||
* 创建备份
|
||||
*
|
||||
* @return array 备份数据
|
||||
*/
|
||||
public function create_backup(): array {
|
||||
return $this->export( true );
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复备份
|
||||
*
|
||||
* @param array $backup 备份数据
|
||||
* @return array 恢复结果
|
||||
*/
|
||||
public function restore_backup( array $backup ): array {
|
||||
return $this->import( $backup, false );
|
||||
}
|
||||
/**
|
||||
* 恢复备份
|
||||
*
|
||||
* @param array $backup 备份数据
|
||||
* @return array 恢复结果
|
||||
*/
|
||||
public function restore_backup( array $backup ): array {
|
||||
return $this->import( $backup, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置为默认配置
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function reset_to_defaults(): bool {
|
||||
foreach ( $this->export_options as $option_name ) {
|
||||
delete_option( $option_name );
|
||||
}
|
||||
/**
|
||||
* 重置为默认配置
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function reset_to_defaults(): bool {
|
||||
foreach ( $this->export_options as $option_name ) {
|
||||
delete_option( $option_name );
|
||||
}
|
||||
|
||||
// 重新初始化默认设置
|
||||
$settings = new Settings();
|
||||
$settings->init_defaults();
|
||||
// 重新初始化默认设置
|
||||
$settings = new Settings();
|
||||
$settings->init_defaults();
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace WPBridge\Core;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,250 +22,250 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class DefaultsManager {
|
||||
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_defaults';
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_defaults';
|
||||
|
||||
/**
|
||||
* 作用范围
|
||||
*/
|
||||
const SCOPE_GLOBAL = 'global';
|
||||
const SCOPE_PLUGIN = 'plugin';
|
||||
const SCOPE_THEME = 'theme';
|
||||
const SCOPE_CORE = 'core';
|
||||
/**
|
||||
* 作用范围
|
||||
*/
|
||||
const SCOPE_GLOBAL = 'global';
|
||||
const SCOPE_PLUGIN = 'plugin';
|
||||
const SCOPE_THEME = 'theme';
|
||||
const SCOPE_CORE = 'core';
|
||||
|
||||
/**
|
||||
* 缓存的默认规则
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $cached_defaults = null;
|
||||
/**
|
||||
* 缓存的默认规则
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $cached_defaults = null;
|
||||
|
||||
/**
|
||||
* 获取所有默认规则
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->cached_defaults ) {
|
||||
$this->cached_defaults = get_option( self::OPTION_NAME, array() );
|
||||
$this->ensure_defaults();
|
||||
}
|
||||
return $this->cached_defaults;
|
||||
}
|
||||
/**
|
||||
* 获取所有默认规则
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->cached_defaults ) {
|
||||
$this->cached_defaults = get_option( self::OPTION_NAME, [] );
|
||||
$this->ensure_defaults();
|
||||
}
|
||||
return $this->cached_defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定范围的默认规则
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return array
|
||||
*/
|
||||
public function get( string $scope ): array {
|
||||
$defaults = $this->get_all();
|
||||
return $defaults[ $scope ] ?? $this->get_scope_defaults( $scope );
|
||||
}
|
||||
/**
|
||||
* 获取指定范围的默认规则
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return array
|
||||
*/
|
||||
public function get( string $scope ): array {
|
||||
$defaults = $this->get_all();
|
||||
return $defaults[ $scope ] ?? $this->get_scope_defaults( $scope );
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置指定范围的默认规则
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @param array $rules 规则数据
|
||||
* @return bool
|
||||
*/
|
||||
public function set( string $scope, array $rules ): bool {
|
||||
$defaults = $this->get_all();
|
||||
$defaults[ $scope ] = array_merge(
|
||||
$this->get_scope_defaults( $scope ),
|
||||
$rules
|
||||
);
|
||||
$defaults[ $scope ]['updated_at'] = current_time( 'mysql' );
|
||||
/**
|
||||
* 设置指定范围的默认规则
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @param array $rules 规则数据
|
||||
* @return bool
|
||||
*/
|
||||
public function set( string $scope, array $rules ): bool {
|
||||
$defaults = $this->get_all();
|
||||
$defaults[ $scope ] = array_merge(
|
||||
$this->get_scope_defaults( $scope ),
|
||||
$rules
|
||||
);
|
||||
$defaults[ $scope ]['updated_at'] = current_time( 'mysql' );
|
||||
|
||||
$this->cached_defaults = $defaults;
|
||||
return update_option( self::OPTION_NAME, $defaults, false );
|
||||
}
|
||||
$this->cached_defaults = $defaults;
|
||||
return update_option( self::OPTION_NAME, $defaults, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认源顺序
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return array 源键列表(按优先级排序)
|
||||
*/
|
||||
public function get_source_order( string $scope ): array {
|
||||
$rules = $this->get( $scope );
|
||||
return $rules['source_order'] ?? array( 'wenpai-mirror', 'wporg' );
|
||||
}
|
||||
/**
|
||||
* 获取默认源顺序
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return array 源键列表(按优先级排序)
|
||||
*/
|
||||
public function get_source_order( string $scope ): array {
|
||||
$rules = $this->get( $scope );
|
||||
return $rules['source_order'] ?? [ 'wenpai-mirror', 'wporg' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认源顺序
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @param array $source_order 源键列表
|
||||
* @return bool
|
||||
*/
|
||||
public function set_source_order( string $scope, array $source_order ): bool {
|
||||
return $this->set( $scope, array( 'source_order' => $source_order ) );
|
||||
}
|
||||
/**
|
||||
* 设置默认源顺序
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @param array $source_order 源键列表
|
||||
* @return bool
|
||||
*/
|
||||
public function set_source_order( string $scope, array $source_order ): bool {
|
||||
return $this->set( $scope, [ 'source_order' => $source_order ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认更新源列表
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @param SourceRegistry $source_registry 源注册表
|
||||
* @return array 源列表(按优先级排序)
|
||||
*/
|
||||
public function get_default_sources( string $scope, SourceRegistry $source_registry ): array {
|
||||
$source_order = $this->get_source_order( $scope );
|
||||
$sources = array();
|
||||
$priority = 100;
|
||||
/**
|
||||
* 获取默认更新源列表
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @param SourceRegistry $source_registry 源注册表
|
||||
* @return array 源列表(按优先级排序)
|
||||
*/
|
||||
public function get_default_sources( string $scope, SourceRegistry $source_registry ): array {
|
||||
$source_order = $this->get_source_order( $scope );
|
||||
$sources = [];
|
||||
$priority = 100;
|
||||
|
||||
foreach ( $source_order as $source_key ) {
|
||||
$source = $source_registry->get( $source_key );
|
||||
if ( $source && ! empty( $source['enabled'] ) ) {
|
||||
$source['priority'] = $priority;
|
||||
$sources[] = $source;
|
||||
$priority -= 10;
|
||||
}
|
||||
}
|
||||
foreach ( $source_order as $source_key ) {
|
||||
$source = $source_registry->get( $source_key );
|
||||
if ( $source && ! empty( $source['enabled'] ) ) {
|
||||
$source['priority'] = $priority;
|
||||
$sources[] = $source;
|
||||
$priority -= 10;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有配置的源可用,回退到 WordPress.org
|
||||
if ( empty( $sources ) ) {
|
||||
$rules = $this->get( $scope );
|
||||
if ( ! empty( $rules['fallback_to_wporg'] ) ) {
|
||||
$wporg = $source_registry->get( 'wporg' );
|
||||
if ( $wporg && ! empty( $wporg['enabled'] ) ) {
|
||||
$wporg['priority'] = 1;
|
||||
$sources[] = $wporg;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果没有配置的源可用,回退到 WordPress.org
|
||||
if ( empty( $sources ) ) {
|
||||
$rules = $this->get( $scope );
|
||||
if ( ! empty( $rules['fallback_to_wporg'] ) ) {
|
||||
$wporg = $source_registry->get( 'wporg' );
|
||||
if ( $wporg && ! empty( $wporg['enabled'] ) ) {
|
||||
$wporg['priority'] = 1;
|
||||
$sources[] = $wporg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sources;
|
||||
}
|
||||
return $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要签名验证
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return bool
|
||||
*/
|
||||
public function is_signature_required( string $scope ): bool {
|
||||
$rules = $this->get( $scope );
|
||||
return ! empty( $rules['signature_required'] );
|
||||
}
|
||||
/**
|
||||
* 是否需要签名验证
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return bool
|
||||
*/
|
||||
public function is_signature_required( string $scope ): bool {
|
||||
$rules = $this->get( $scope );
|
||||
return ! empty( $rules['signature_required'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否允许无签名包
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return bool
|
||||
*/
|
||||
public function is_unsigned_allowed( string $scope ): bool {
|
||||
$rules = $this->get( $scope );
|
||||
return ! empty( $rules['allow_unsigned'] );
|
||||
}
|
||||
/**
|
||||
* 是否允许无签名包
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return bool
|
||||
*/
|
||||
public function is_unsigned_allowed( string $scope ): bool {
|
||||
$rules = $this->get( $scope );
|
||||
return ! empty( $rules['allow_unsigned'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否允许预发布版本
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return bool
|
||||
*/
|
||||
public function is_prerelease_allowed( string $scope ): bool {
|
||||
$rules = $this->get( $scope );
|
||||
return ! empty( $rules['allow_prerelease'] );
|
||||
}
|
||||
/**
|
||||
* 是否允许预发布版本
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return bool
|
||||
*/
|
||||
public function is_prerelease_allowed( string $scope ): bool {
|
||||
$rules = $this->get( $scope );
|
||||
return ! empty( $rules['allow_prerelease'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最低信任阈值
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return int
|
||||
*/
|
||||
public function get_trust_floor( string $scope ): int {
|
||||
$rules = $this->get( $scope );
|
||||
return (int) ( $rules['trust_floor'] ?? 0 );
|
||||
}
|
||||
/**
|
||||
* 获取最低信任阈值
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return int
|
||||
*/
|
||||
public function get_trust_floor( string $scope ): int {
|
||||
$rules = $this->get( $scope );
|
||||
return (int) ( $rules['trust_floor'] ?? 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保默认规则存在
|
||||
*/
|
||||
private function ensure_defaults(): void {
|
||||
$needs_update = false;
|
||||
$scopes = array( self::SCOPE_GLOBAL, self::SCOPE_PLUGIN, self::SCOPE_THEME, self::SCOPE_CORE );
|
||||
/**
|
||||
* 确保默认规则存在
|
||||
*/
|
||||
private function ensure_defaults(): void {
|
||||
$needs_update = false;
|
||||
$scopes = [ self::SCOPE_GLOBAL, self::SCOPE_PLUGIN, self::SCOPE_THEME, self::SCOPE_CORE ];
|
||||
|
||||
foreach ( $scopes as $scope ) {
|
||||
if ( ! isset( $this->cached_defaults[ $scope ] ) ) {
|
||||
$this->cached_defaults[ $scope ] = $this->get_scope_defaults( $scope );
|
||||
$needs_update = true;
|
||||
}
|
||||
}
|
||||
foreach ( $scopes as $scope ) {
|
||||
if ( ! isset( $this->cached_defaults[ $scope ] ) ) {
|
||||
$this->cached_defaults[ $scope ] = $this->get_scope_defaults( $scope );
|
||||
$needs_update = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $needs_update ) {
|
||||
update_option( self::OPTION_NAME, $this->cached_defaults, false );
|
||||
}
|
||||
}
|
||||
if ( $needs_update ) {
|
||||
update_option( self::OPTION_NAME, $this->cached_defaults, false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取范围的默认值
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return array
|
||||
*/
|
||||
private function get_scope_defaults( string $scope ): array {
|
||||
$base = array(
|
||||
'source_order' => array( 'wenpai-mirror', 'wporg' ),
|
||||
'signature_required' => false,
|
||||
'allow_unsigned' => true,
|
||||
'allow_prerelease' => false,
|
||||
'trust_floor' => 0,
|
||||
'fallback_to_wporg' => true,
|
||||
'policy' => array(),
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
);
|
||||
/**
|
||||
* 获取范围的默认值
|
||||
*
|
||||
* @param string $scope 作用范围
|
||||
* @return array
|
||||
*/
|
||||
private function get_scope_defaults( string $scope ): array {
|
||||
$base = [
|
||||
'source_order' => [ 'wenpai-mirror', 'wporg' ],
|
||||
'signature_required' => false,
|
||||
'allow_unsigned' => true,
|
||||
'allow_prerelease' => false,
|
||||
'trust_floor' => 0,
|
||||
'fallback_to_wporg' => true,
|
||||
'policy' => [],
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
];
|
||||
|
||||
// 根据范围调整默认值
|
||||
switch ( $scope ) {
|
||||
case self::SCOPE_CORE:
|
||||
$base['source_order'] = array( 'wporg' );
|
||||
$base['trust_floor'] = 90;
|
||||
break;
|
||||
// 根据范围调整默认值
|
||||
switch ( $scope ) {
|
||||
case self::SCOPE_CORE:
|
||||
$base['source_order'] = [ 'wporg' ];
|
||||
$base['trust_floor'] = 90;
|
||||
break;
|
||||
|
||||
case self::SCOPE_PLUGIN:
|
||||
case self::SCOPE_THEME:
|
||||
$base['source_order'] = array( 'wenpai-mirror', 'wporg' );
|
||||
break;
|
||||
case self::SCOPE_PLUGIN:
|
||||
case self::SCOPE_THEME:
|
||||
$base['source_order'] = [ 'wenpai-mirror', 'wporg' ];
|
||||
break;
|
||||
|
||||
case self::SCOPE_GLOBAL:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
case self::SCOPE_GLOBAL:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
return $base;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置为默认值
|
||||
*
|
||||
* @param string|null $scope 作用范围,null 表示全部重置
|
||||
* @return bool
|
||||
*/
|
||||
public function reset( ?string $scope = null ): bool {
|
||||
if ( null === $scope ) {
|
||||
$this->cached_defaults = null;
|
||||
return delete_option( self::OPTION_NAME );
|
||||
}
|
||||
/**
|
||||
* 重置为默认值
|
||||
*
|
||||
* @param string|null $scope 作用范围,null 表示全部重置
|
||||
* @return bool
|
||||
*/
|
||||
public function reset( ?string $scope = null ): bool {
|
||||
if ( null === $scope ) {
|
||||
$this->cached_defaults = null;
|
||||
return delete_option( self::OPTION_NAME );
|
||||
}
|
||||
|
||||
$defaults = $this->get_all();
|
||||
$defaults[ $scope ] = $this->get_scope_defaults( $scope );
|
||||
$this->cached_defaults = $defaults;
|
||||
return update_option( self::OPTION_NAME, $defaults, false );
|
||||
}
|
||||
$defaults = $this->get_all();
|
||||
$defaults[ $scope ] = $this->get_scope_defaults( $scope );
|
||||
$this->cached_defaults = $defaults;
|
||||
return update_option( self::OPTION_NAME, $defaults, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->cached_defaults = null;
|
||||
}
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->cached_defaults = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace WPBridge\Core;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,397 +22,385 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class ItemSourceManager {
|
||||
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_item_sources';
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_item_sources';
|
||||
|
||||
/**
|
||||
* 项目类型
|
||||
*/
|
||||
const TYPE_PLUGIN = 'plugin';
|
||||
const TYPE_THEME = 'theme';
|
||||
const TYPE_MUPLUGIN = 'mu-plugin';
|
||||
const TYPE_DROPIN = 'dropin';
|
||||
/**
|
||||
* 项目类型
|
||||
*/
|
||||
const TYPE_PLUGIN = 'plugin';
|
||||
const TYPE_THEME = 'theme';
|
||||
const TYPE_MUPLUGIN = 'mu-plugin';
|
||||
const TYPE_DROPIN = 'dropin';
|
||||
|
||||
/**
|
||||
* 配置模式
|
||||
*/
|
||||
const MODE_DEFAULT = 'default';
|
||||
const MODE_CUSTOM = 'custom';
|
||||
const MODE_DISABLED = 'disabled';
|
||||
/**
|
||||
* 配置模式
|
||||
*/
|
||||
const MODE_DEFAULT = 'default';
|
||||
const MODE_CUSTOM = 'custom';
|
||||
const MODE_DISABLED = 'disabled';
|
||||
|
||||
/**
|
||||
* 缓存的配置
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $cached_configs = null;
|
||||
/**
|
||||
* 缓存的配置
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $cached_configs = null;
|
||||
|
||||
/**
|
||||
* 源注册表
|
||||
*
|
||||
* @var SourceRegistry
|
||||
*/
|
||||
private SourceRegistry $source_registry;
|
||||
/**
|
||||
* 源注册表
|
||||
*
|
||||
* @var SourceRegistry
|
||||
*/
|
||||
private SourceRegistry $source_registry;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param SourceRegistry $source_registry 源注册表
|
||||
*/
|
||||
public function __construct( SourceRegistry $source_registry ) {
|
||||
$this->source_registry = $source_registry;
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param SourceRegistry $source_registry 源注册表
|
||||
*/
|
||||
public function __construct( SourceRegistry $source_registry ) {
|
||||
$this->source_registry = $source_registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有项目配置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->cached_configs ) {
|
||||
$this->cached_configs = get_option( self::OPTION_NAME, array() );
|
||||
}
|
||||
return $this->cached_configs;
|
||||
}
|
||||
/**
|
||||
* 获取所有项目配置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->cached_configs ) {
|
||||
$this->cached_configs = get_option( self::OPTION_NAME, [] );
|
||||
}
|
||||
return $this->cached_configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按类型获取项目配置
|
||||
*
|
||||
* @param string $type 项目类型
|
||||
* @return array
|
||||
*/
|
||||
public function get_by_type( string $type ): array {
|
||||
return array_filter( $this->get_all(), fn( $c ) => ( $c['item_type'] ?? '' ) === $type );
|
||||
}
|
||||
/**
|
||||
* 按类型获取项目配置
|
||||
*
|
||||
* @param string $type 项目类型
|
||||
* @return array
|
||||
*/
|
||||
public function get_by_type( string $type ): array {
|
||||
return array_filter( $this->get_all(), fn( $c ) => ( $c['item_type'] ?? '' ) === $type );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个项目配置
|
||||
*
|
||||
* @param string $item_key 项目键(plugin_basename 或主题目录名)
|
||||
* @return array|null
|
||||
*/
|
||||
public function get( string $item_key ): ?array {
|
||||
foreach ( $this->get_all() as $config ) {
|
||||
if ( ( $config['item_key'] ?? '' ) === $item_key ) {
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 获取单个项目配置
|
||||
*
|
||||
* @param string $item_key 项目键(plugin_basename 或主题目录名)
|
||||
* @return array|null
|
||||
*/
|
||||
public function get( string $item_key ): ?array {
|
||||
foreach ( $this->get_all() as $config ) {
|
||||
if ( ( $config['item_key'] ?? '' ) === $item_key ) {
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 DID 获取项目配置
|
||||
*
|
||||
* @param string $did 项目 DID
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_by_did( string $did ): ?array {
|
||||
foreach ( $this->get_all() as $config ) {
|
||||
if ( ( $config['item_did'] ?? '' ) === $did ) {
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 通过 DID 获取项目配置
|
||||
*
|
||||
* @param string $did 项目 DID
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_by_did( string $did ): ?array {
|
||||
foreach ( $this->get_all() as $config ) {
|
||||
if ( ( $config['item_did'] ?? '' ) === $did ) {
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置项目配置
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param array $config 配置数据
|
||||
* @return bool
|
||||
*/
|
||||
public function set( string $item_key, array $config ): bool {
|
||||
$configs = $this->get_all();
|
||||
$found = false;
|
||||
/**
|
||||
* 设置项目配置
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param array $config 配置数据
|
||||
* @return bool
|
||||
*/
|
||||
public function set( string $item_key, array $config ): bool {
|
||||
$configs = $this->get_all();
|
||||
$found = false;
|
||||
|
||||
foreach ( $configs as $index => $existing ) {
|
||||
if ( ( $existing['item_key'] ?? '' ) === $item_key ) {
|
||||
$configs[ $index ] = array_merge( $existing, $config );
|
||||
$configs[ $index ]['item_key'] = $item_key;
|
||||
$configs[ $index ]['updated_at'] = current_time( 'mysql' );
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach ( $configs as $index => $existing ) {
|
||||
if ( ( $existing['item_key'] ?? '' ) === $item_key ) {
|
||||
$configs[ $index ] = array_merge( $existing, $config );
|
||||
$configs[ $index ]['item_key'] = $item_key;
|
||||
$configs[ $index ]['updated_at'] = current_time( 'mysql' );
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $found ) {
|
||||
$config = $this->normalize_config( $config );
|
||||
$config['item_key'] = $item_key;
|
||||
$config['created_at'] = current_time( 'mysql' );
|
||||
$config['updated_at'] = current_time( 'mysql' );
|
||||
$configs[] = $config;
|
||||
}
|
||||
if ( ! $found ) {
|
||||
$config = $this->normalize_config( $config );
|
||||
$config['item_key'] = $item_key;
|
||||
$config['created_at'] = current_time( 'mysql' );
|
||||
$config['updated_at'] = current_time( 'mysql' );
|
||||
$configs[] = $config;
|
||||
}
|
||||
|
||||
$this->cached_configs = $configs;
|
||||
return update_option( self::OPTION_NAME, $configs, false );
|
||||
}
|
||||
$this->cached_configs = $configs;
|
||||
return update_option( self::OPTION_NAME, $configs, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项目配置
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $item_key ): bool {
|
||||
$configs = $this->get_all();
|
||||
/**
|
||||
* 删除项目配置
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $item_key ): bool {
|
||||
$configs = $this->get_all();
|
||||
|
||||
foreach ( $configs as $index => $config ) {
|
||||
if ( ( $config['item_key'] ?? '' ) === $item_key ) {
|
||||
unset( $configs[ $index ] );
|
||||
$configs = array_values( $configs );
|
||||
$this->cached_configs = $configs;
|
||||
return update_option( self::OPTION_NAME, $configs, false );
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
foreach ( $configs as $index => $config ) {
|
||||
if ( ( $config['item_key'] ?? '' ) === $item_key ) {
|
||||
unset( $configs[ $index ] );
|
||||
$configs = array_values( $configs );
|
||||
$this->cached_configs = $configs;
|
||||
return update_option( self::OPTION_NAME, $configs, false );
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置项目的更新源
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $source_key 源键
|
||||
* @param int $priority 优先级
|
||||
* @return bool
|
||||
*/
|
||||
public function set_source( string $item_key, string $source_key, int $priority = 50 ): bool {
|
||||
$config = $this->get( $item_key ) ?? array();
|
||||
$source_ids = $config['source_ids'] ?? array();
|
||||
/**
|
||||
* 设置项目的更新源
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $source_key 源键
|
||||
* @param int $priority 优先级
|
||||
* @return bool
|
||||
*/
|
||||
public function set_source( string $item_key, string $source_key, int $priority = 50 ): bool {
|
||||
$config = $this->get( $item_key ) ?? [];
|
||||
$source_ids = $config['source_ids'] ?? [];
|
||||
|
||||
// 检查源是否存在
|
||||
if ( ! $this->source_registry->get( $source_key ) ) {
|
||||
return false;
|
||||
}
|
||||
// 检查源是否存在
|
||||
if ( ! $this->source_registry->get( $source_key ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 添加或更新源
|
||||
$source_ids[ $source_key ] = $priority;
|
||||
arsort( $source_ids ); // 按优先级排序
|
||||
// 添加或更新源
|
||||
$source_ids[ $source_key ] = $priority;
|
||||
arsort( $source_ids ); // 按优先级排序
|
||||
|
||||
return $this->set(
|
||||
$item_key,
|
||||
array(
|
||||
'mode' => self::MODE_CUSTOM,
|
||||
'source_ids' => $source_ids,
|
||||
)
|
||||
);
|
||||
}
|
||||
return $this->set( $item_key, [
|
||||
'mode' => self::MODE_CUSTOM,
|
||||
'source_ids' => $source_ids,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除项目的更新源
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $source_key 源键
|
||||
* @return bool
|
||||
*/
|
||||
public function remove_source( string $item_key, string $source_key ): bool {
|
||||
$config = $this->get( $item_key );
|
||||
if ( ! $config ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 移除项目的更新源
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $source_key 源键
|
||||
* @return bool
|
||||
*/
|
||||
public function remove_source( string $item_key, string $source_key ): bool {
|
||||
$config = $this->get( $item_key );
|
||||
if ( ! $config ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$source_ids = $config['source_ids'] ?? array();
|
||||
unset( $source_ids[ $source_key ] );
|
||||
$source_ids = $config['source_ids'] ?? [];
|
||||
unset( $source_ids[ $source_key ] );
|
||||
|
||||
// 如果没有自定义源了,切回默认模式
|
||||
$mode = empty( $source_ids ) ? self::MODE_DEFAULT : self::MODE_CUSTOM;
|
||||
// 如果没有自定义源了,切回默认模式
|
||||
$mode = empty( $source_ids ) ? self::MODE_DEFAULT : self::MODE_CUSTOM;
|
||||
|
||||
return $this->set(
|
||||
$item_key,
|
||||
array(
|
||||
'mode' => $mode,
|
||||
'source_ids' => $source_ids,
|
||||
)
|
||||
);
|
||||
}
|
||||
return $this->set( $item_key, [
|
||||
'mode' => $mode,
|
||||
'source_ids' => $source_ids,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用项目更新
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return bool
|
||||
*/
|
||||
public function disable_updates( string $item_key ): bool {
|
||||
return $this->set( $item_key, array( 'mode' => self::MODE_DISABLED ) );
|
||||
}
|
||||
/**
|
||||
* 禁用项目更新
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return bool
|
||||
*/
|
||||
public function disable_updates( string $item_key ): bool {
|
||||
return $this->set( $item_key, [ 'mode' => self::MODE_DISABLED ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用项目更新(切回默认)
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return bool
|
||||
*/
|
||||
public function enable_updates( string $item_key ): bool {
|
||||
return $this->set( $item_key, array( 'mode' => self::MODE_DEFAULT ) );
|
||||
}
|
||||
/**
|
||||
* 启用项目更新(切回默认)
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return bool
|
||||
*/
|
||||
public function enable_updates( string $item_key ): bool {
|
||||
return $this->set( $item_key, [ 'mode' => self::MODE_DEFAULT ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 固定项目到特定源
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $source_key 源键
|
||||
* @return bool
|
||||
*/
|
||||
public function pin_to_source( string $item_key, string $source_key ): bool {
|
||||
return $this->set(
|
||||
$item_key,
|
||||
array(
|
||||
'mode' => self::MODE_CUSTOM,
|
||||
'source_ids' => array( $source_key => 100 ),
|
||||
'pinned' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 固定项目到特定源
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $source_key 源键
|
||||
* @return bool
|
||||
*/
|
||||
public function pin_to_source( string $item_key, string $source_key ): bool {
|
||||
return $this->set( $item_key, [
|
||||
'mode' => self::MODE_CUSTOM,
|
||||
'source_ids' => [ $source_key => 100 ],
|
||||
'pinned' => true,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目的有效更新源列表
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param DefaultsManager $defaults 默认规则管理器
|
||||
* @return array 源列表(按优先级排序)
|
||||
*/
|
||||
public function get_effective_sources( string $item_key, DefaultsManager $defaults ): array {
|
||||
$config = $this->get( $item_key );
|
||||
/**
|
||||
* 获取项目的有效更新源列表
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param DefaultsManager $defaults 默认规则管理器
|
||||
* @return array 源列表(按优先级排序)
|
||||
*/
|
||||
public function get_effective_sources( string $item_key, DefaultsManager $defaults ): array {
|
||||
$config = $this->get( $item_key );
|
||||
|
||||
// 如果禁用更新,返回空
|
||||
if ( $config && ( $config['mode'] ?? '' ) === self::MODE_DISABLED ) {
|
||||
return array();
|
||||
}
|
||||
// 如果禁用更新,返回空
|
||||
if ( $config && ( $config['mode'] ?? '' ) === self::MODE_DISABLED ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 如果有自定义配置,使用自定义源
|
||||
if ( $config && ( $config['mode'] ?? '' ) === self::MODE_CUSTOM ) {
|
||||
$source_ids = $config['source_ids'] ?? array();
|
||||
$sources = array();
|
||||
// 如果有自定义配置,使用自定义源
|
||||
if ( $config && ( $config['mode'] ?? '' ) === self::MODE_CUSTOM ) {
|
||||
$source_ids = $config['source_ids'] ?? [];
|
||||
$sources = [];
|
||||
|
||||
foreach ( $source_ids as $source_key => $priority ) {
|
||||
$source = $this->source_registry->get( $source_key );
|
||||
if ( $source && ! empty( $source['enabled'] ) ) {
|
||||
$source['priority'] = $priority;
|
||||
$sources[] = $source;
|
||||
}
|
||||
}
|
||||
foreach ( $source_ids as $source_key => $priority ) {
|
||||
$source = $this->source_registry->get( $source_key );
|
||||
if ( $source && ! empty( $source['enabled'] ) ) {
|
||||
$source['priority'] = $priority;
|
||||
$sources[] = $source;
|
||||
}
|
||||
}
|
||||
|
||||
// 按优先级排序
|
||||
usort( $sources, fn( $a, $b ) => ( $b['priority'] ?? 0 ) - ( $a['priority'] ?? 0 ) );
|
||||
return $sources;
|
||||
}
|
||||
// 按优先级排序
|
||||
usort( $sources, fn( $a, $b ) => ( $b['priority'] ?? 0 ) - ( $a['priority'] ?? 0 ) );
|
||||
return $sources;
|
||||
}
|
||||
|
||||
// 使用默认源 - 从配置或 item_key 前缀推断类型
|
||||
$item_type = $this->resolve_item_type( $item_key, $config );
|
||||
return $defaults->get_default_sources( $item_type, $this->source_registry );
|
||||
}
|
||||
// 使用默认源 - 从配置或 item_key 前缀推断类型
|
||||
$item_type = $this->resolve_item_type( $item_key, $config );
|
||||
return $defaults->get_default_sources( $item_type, $this->source_registry );
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析项目类型
|
||||
*
|
||||
* 优先从配置获取,否则从 item_key 前缀推断
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param array|null $config 项目配置(可能为 null)
|
||||
* @return string 项目类型
|
||||
*/
|
||||
private function resolve_item_type( string $item_key, ?array $config ): string {
|
||||
// 优先使用配置中的类型
|
||||
if ( $config && ! empty( $config['item_type'] ) ) {
|
||||
return $config['item_type'];
|
||||
}
|
||||
/**
|
||||
* 解析项目类型
|
||||
*
|
||||
* 优先从配置获取,否则从 item_key 前缀推断
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param array|null $config 项目配置(可能为 null)
|
||||
* @return string 项目类型
|
||||
*/
|
||||
private function resolve_item_type( string $item_key, ?array $config ): string {
|
||||
// 优先使用配置中的类型
|
||||
if ( $config && ! empty( $config['item_type'] ) ) {
|
||||
return $config['item_type'];
|
||||
}
|
||||
|
||||
// 从 item_key 前缀推断类型
|
||||
// 格式: "type:identifier" 例如 "plugin:hello-dolly/hello.php" 或 "theme:flavor"
|
||||
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
|
||||
return self::TYPE_PLUGIN;
|
||||
}
|
||||
// 从 item_key 前缀推断类型
|
||||
// 格式: "type:identifier" 例如 "plugin:hello-dolly/hello.php" 或 "theme:flavor"
|
||||
if ( strpos( $item_key, 'plugin:' ) === 0 ) {
|
||||
return self::TYPE_PLUGIN;
|
||||
}
|
||||
|
||||
if ( strpos( $item_key, 'theme:' ) === 0 ) {
|
||||
return self::TYPE_THEME;
|
||||
}
|
||||
if ( strpos( $item_key, 'theme:' ) === 0 ) {
|
||||
return self::TYPE_THEME;
|
||||
}
|
||||
|
||||
if ( strpos( $item_key, 'mu-plugin:' ) === 0 ) {
|
||||
return self::TYPE_MUPLUGIN;
|
||||
}
|
||||
if ( strpos( $item_key, 'mu-plugin:' ) === 0 ) {
|
||||
return self::TYPE_MUPLUGIN;
|
||||
}
|
||||
|
||||
if ( strpos( $item_key, 'dropin:' ) === 0 ) {
|
||||
return self::TYPE_DROPIN;
|
||||
}
|
||||
if ( strpos( $item_key, 'dropin:' ) === 0 ) {
|
||||
return self::TYPE_DROPIN;
|
||||
}
|
||||
|
||||
// 无前缀时默认为插件类型(向后兼容)
|
||||
return self::TYPE_PLUGIN;
|
||||
}
|
||||
// 无前缀时默认为插件类型(向后兼容)
|
||||
return self::TYPE_PLUGIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置项目配置
|
||||
*
|
||||
* @param array $item_keys 项目键列表
|
||||
* @param string $source_key 源键
|
||||
* @param int $priority 优先级
|
||||
* @return int 成功数量
|
||||
*/
|
||||
public function batch_set_source( array $item_keys, string $source_key, int $priority = 50 ): int {
|
||||
$success = 0;
|
||||
foreach ( $item_keys as $item_key ) {
|
||||
if ( $this->set_source( $item_key, $source_key, $priority ) ) {
|
||||
++$success;
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
/**
|
||||
* 批量设置项目配置
|
||||
*
|
||||
* @param array $item_keys 项目键列表
|
||||
* @param string $source_key 源键
|
||||
* @param int $priority 优先级
|
||||
* @return int 成功数量
|
||||
*/
|
||||
public function batch_set_source( array $item_keys, string $source_key, int $priority = 50 ): int {
|
||||
$success = 0;
|
||||
foreach ( $item_keys as $item_key ) {
|
||||
if ( $this->set_source( $item_key, $source_key, $priority ) ) {
|
||||
$success++;
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量重置为默认
|
||||
*
|
||||
* @param array $item_keys 项目键列表
|
||||
* @return int 成功数量
|
||||
*/
|
||||
public function batch_reset_to_default( array $item_keys ): int {
|
||||
$success = 0;
|
||||
foreach ( $item_keys as $item_key ) {
|
||||
if ( $this->delete( $item_key ) ) {
|
||||
++$success;
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
/**
|
||||
* 批量重置为默认
|
||||
*
|
||||
* @param array $item_keys 项目键列表
|
||||
* @return int 成功数量
|
||||
*/
|
||||
public function batch_reset_to_default( array $item_keys ): int {
|
||||
$success = 0;
|
||||
foreach ( $item_keys as $item_key ) {
|
||||
if ( $this->delete( $item_key ) ) {
|
||||
$success++;
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化配置数据
|
||||
*
|
||||
* @param array $config 配置数据
|
||||
* @return array
|
||||
*/
|
||||
private function normalize_config( array $config ): array {
|
||||
return wp_parse_args(
|
||||
$config,
|
||||
array(
|
||||
'item_key' => '',
|
||||
'item_type' => self::TYPE_PLUGIN,
|
||||
'item_slug' => '',
|
||||
'item_did' => '',
|
||||
'label' => '',
|
||||
'mode' => self::MODE_DEFAULT,
|
||||
'source_ids' => array(),
|
||||
'pinned' => false,
|
||||
'signature_required' => false,
|
||||
'allow_unsigned' => true,
|
||||
'allow_prerelease' => false,
|
||||
'min_version' => '',
|
||||
'max_version' => '',
|
||||
'last_good_version' => '',
|
||||
'metadata' => array(
|
||||
'preconfigured' => false,
|
||||
'installed' => true,
|
||||
),
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 规范化配置数据
|
||||
*
|
||||
* @param array $config 配置数据
|
||||
* @return array
|
||||
*/
|
||||
private function normalize_config( array $config ): array {
|
||||
return wp_parse_args( $config, [
|
||||
'item_key' => '',
|
||||
'item_type' => self::TYPE_PLUGIN,
|
||||
'item_slug' => '',
|
||||
'item_did' => '',
|
||||
'label' => '',
|
||||
'mode' => self::MODE_DEFAULT,
|
||||
'source_ids' => [],
|
||||
'pinned' => false,
|
||||
'signature_required' => false,
|
||||
'allow_unsigned' => true,
|
||||
'allow_prerelease' => false,
|
||||
'min_version' => '',
|
||||
'max_version' => '',
|
||||
'last_good_version' => '',
|
||||
'metadata' => [
|
||||
'preconfigured' => false,
|
||||
'installed' => true,
|
||||
],
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->cached_configs = null;
|
||||
}
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->cached_configs = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\Core;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,51 +17,51 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class Loader {
|
||||
|
||||
/**
|
||||
* 命名空间前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static string $namespace_prefix = 'WPBridge\\';
|
||||
/**
|
||||
* 命名空间前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static string $namespace_prefix = 'WPBridge\\';
|
||||
|
||||
/**
|
||||
* 基础目录
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static string $base_dir = '';
|
||||
/**
|
||||
* 基础目录
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static string $base_dir = '';
|
||||
|
||||
/**
|
||||
* 注册自动加载器
|
||||
*/
|
||||
public static function register(): void {
|
||||
self::$base_dir = WPBRIDGE_PATH . 'includes/';
|
||||
spl_autoload_register( array( __CLASS__, 'autoload' ) );
|
||||
}
|
||||
/**
|
||||
* 注册自动加载器
|
||||
*/
|
||||
public static function register(): void {
|
||||
self::$base_dir = WPBRIDGE_PATH . 'includes/';
|
||||
spl_autoload_register( [ __CLASS__, 'autoload' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动加载类
|
||||
*
|
||||
* @param string $class 完整类名
|
||||
*/
|
||||
public static function autoload( string $class ): void {
|
||||
// 检查是否是我们的命名空间
|
||||
$len = strlen( self::$namespace_prefix );
|
||||
if ( strncmp( self::$namespace_prefix, $class, $len ) !== 0 ) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 自动加载类
|
||||
*
|
||||
* @param string $class 完整类名
|
||||
*/
|
||||
public static function autoload( string $class ): void {
|
||||
// 检查是否是我们的命名空间
|
||||
$len = strlen( self::$namespace_prefix );
|
||||
if ( strncmp( self::$namespace_prefix, $class, $len ) !== 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取相对类名
|
||||
$relative_class = substr( $class, $len );
|
||||
// 获取相对类名
|
||||
$relative_class = substr( $class, $len );
|
||||
|
||||
// 转换为文件路径
|
||||
$file = self::$base_dir . str_replace( '\\', '/', $relative_class ) . '.php';
|
||||
// 转换为文件路径
|
||||
$file = self::$base_dir . str_replace( '\\', '/', $relative_class ) . '.php';
|
||||
|
||||
// 如果文件存在则加载
|
||||
if ( file_exists( $file ) ) {
|
||||
require_once $file;
|
||||
}
|
||||
}
|
||||
// 如果文件存在则加载
|
||||
if ( file_exists( $file ) ) {
|
||||
require_once $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 立即注册自动加载器
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\Core;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,162 +17,159 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class Logger {
|
||||
|
||||
/**
|
||||
* 日志级别
|
||||
*/
|
||||
const LEVEL_DEBUG = 'debug';
|
||||
const LEVEL_INFO = 'info';
|
||||
const LEVEL_WARNING = 'warning';
|
||||
const LEVEL_ERROR = 'error';
|
||||
/**
|
||||
* 日志级别
|
||||
*/
|
||||
const LEVEL_DEBUG = 'debug';
|
||||
const LEVEL_INFO = 'info';
|
||||
const LEVEL_WARNING = 'warning';
|
||||
const LEVEL_ERROR = 'error';
|
||||
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_LOGS = 'wpbridge_logs';
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_LOGS = 'wpbridge_logs';
|
||||
|
||||
/**
|
||||
* 最大日志条数
|
||||
*/
|
||||
const MAX_LOGS = 100;
|
||||
/**
|
||||
* 最大日志条数
|
||||
*/
|
||||
const MAX_LOGS = 100;
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings|null
|
||||
*/
|
||||
private static ?Settings $settings = null;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings|null
|
||||
*/
|
||||
private static ?Settings $settings = null;
|
||||
|
||||
/**
|
||||
* 设置 Settings 实例
|
||||
*
|
||||
* @param Settings $settings
|
||||
*/
|
||||
public static function set_settings( Settings $settings ): void {
|
||||
self::$settings = $settings;
|
||||
}
|
||||
/**
|
||||
* 设置 Settings 实例
|
||||
*
|
||||
* @param Settings $settings
|
||||
*/
|
||||
public static function set_settings( Settings $settings ): void {
|
||||
self::$settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录调试日志
|
||||
*
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
public static function debug( string $message, array $context = array() ): void {
|
||||
self::log( self::LEVEL_DEBUG, $message, $context );
|
||||
}
|
||||
/**
|
||||
* 记录调试日志
|
||||
*
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
public static function debug( string $message, array $context = [] ): void {
|
||||
self::log( self::LEVEL_DEBUG, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录信息日志
|
||||
*
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
public static function info( string $message, array $context = array() ): void {
|
||||
self::log( self::LEVEL_INFO, $message, $context );
|
||||
}
|
||||
/**
|
||||
* 记录信息日志
|
||||
*
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
public static function info( string $message, array $context = [] ): void {
|
||||
self::log( self::LEVEL_INFO, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录警告日志
|
||||
*
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
public static function warning( string $message, array $context = array() ): void {
|
||||
self::log( self::LEVEL_WARNING, $message, $context );
|
||||
}
|
||||
/**
|
||||
* 记录警告日志
|
||||
*
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
public static function warning( string $message, array $context = [] ): void {
|
||||
self::log( self::LEVEL_WARNING, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
*
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
public static function error( string $message, array $context = array() ): void {
|
||||
self::log( self::LEVEL_ERROR, $message, $context );
|
||||
}
|
||||
/**
|
||||
* 记录错误日志
|
||||
*
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
public static function error( string $message, array $context = [] ): void {
|
||||
self::log( self::LEVEL_ERROR, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
*
|
||||
* @param string $level 日志级别
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
public static function log( string $level, string $message, array $context = array() ): void {
|
||||
// 检查是否启用调试模式(错误日志始终记录)
|
||||
if ( $level !== self::LEVEL_ERROR ) {
|
||||
if ( null === self::$settings ) {
|
||||
self::$settings = new Settings();
|
||||
}
|
||||
if ( ! self::$settings->is_debug() ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 记录日志
|
||||
*
|
||||
* @param string $level 日志级别
|
||||
* @param string $message 消息
|
||||
* @param array $context 上下文
|
||||
*/
|
||||
public static function log( string $level, string $message, array $context = [] ): void {
|
||||
// 检查是否启用调试模式(错误日志始终记录)
|
||||
if ( $level !== self::LEVEL_ERROR ) {
|
||||
if ( null === self::$settings ) {
|
||||
self::$settings = new Settings();
|
||||
}
|
||||
if ( ! self::$settings->is_debug() ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$logs = get_option( self::OPTION_LOGS, array() );
|
||||
$logs = get_option( self::OPTION_LOGS, [] );
|
||||
|
||||
// 添加新日志
|
||||
$logs[] = array(
|
||||
'time' => current_time( 'mysql' ),
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'context' => self::sanitize_context( $context ),
|
||||
);
|
||||
// 添加新日志
|
||||
$logs[] = [
|
||||
'time' => current_time( 'mysql' ),
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'context' => self::sanitize_context( $context ),
|
||||
];
|
||||
|
||||
// 限制日志数量
|
||||
if ( count( $logs ) > self::MAX_LOGS ) {
|
||||
$logs = array_slice( $logs, -self::MAX_LOGS );
|
||||
}
|
||||
// 限制日志数量
|
||||
if ( count( $logs ) > self::MAX_LOGS ) {
|
||||
$logs = array_slice( $logs, -self::MAX_LOGS );
|
||||
}
|
||||
|
||||
update_option( self::OPTION_LOGS, $logs, false );
|
||||
}
|
||||
update_option( self::OPTION_LOGS, $logs, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理上下文数据(脱敏)
|
||||
*
|
||||
* @param array $context 上下文
|
||||
* @return array
|
||||
*/
|
||||
private static function sanitize_context( array $context ): array {
|
||||
$sensitive_keys = array( 'auth_token', 'api_key', 'password', 'secret' );
|
||||
/**
|
||||
* 清理上下文数据(脱敏)
|
||||
*
|
||||
* @param array $context 上下文
|
||||
* @return array
|
||||
*/
|
||||
private static function sanitize_context( array $context ): array {
|
||||
$sensitive_keys = [ 'auth_token', 'api_key', 'password', 'secret' ];
|
||||
|
||||
foreach ( $context as $key => $value ) {
|
||||
if ( in_array( strtolower( $key ), $sensitive_keys, true ) ) {
|
||||
$context[ $key ] = '***REDACTED***';
|
||||
} elseif ( is_array( $value ) ) {
|
||||
$context[ $key ] = self::sanitize_context( $value );
|
||||
}
|
||||
}
|
||||
foreach ( $context as $key => $value ) {
|
||||
if ( in_array( strtolower( $key ), $sensitive_keys, true ) ) {
|
||||
$context[ $key ] = '***REDACTED***';
|
||||
} elseif ( is_array( $value ) ) {
|
||||
$context[ $key ] = self::sanitize_context( $value );
|
||||
}
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有日志
|
||||
*
|
||||
* @param string|null $level 过滤级别
|
||||
* @return array
|
||||
*/
|
||||
public static function get_logs( ?string $level = null ): array {
|
||||
$logs = get_option( self::OPTION_LOGS, array() );
|
||||
/**
|
||||
* 获取所有日志
|
||||
*
|
||||
* @param string|null $level 过滤级别
|
||||
* @return array
|
||||
*/
|
||||
public static function get_logs( ?string $level = null ): array {
|
||||
$logs = get_option( self::OPTION_LOGS, [] );
|
||||
|
||||
if ( null !== $level ) {
|
||||
$logs = array_filter(
|
||||
$logs,
|
||||
function ( $log ) use ( $level ) {
|
||||
return $log['level'] === $level;
|
||||
}
|
||||
);
|
||||
}
|
||||
if ( null !== $level ) {
|
||||
$logs = array_filter( $logs, function( $log ) use ( $level ) {
|
||||
return $log['level'] === $level;
|
||||
} );
|
||||
}
|
||||
|
||||
// 按时间倒序
|
||||
return array_reverse( $logs );
|
||||
}
|
||||
// 按时间倒序
|
||||
return array_reverse( $logs );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有日志
|
||||
*/
|
||||
public static function clear(): void {
|
||||
delete_option( self::OPTION_LOGS );
|
||||
}
|
||||
/**
|
||||
* 清除所有日志
|
||||
*/
|
||||
public static function clear(): void {
|
||||
delete_option( self::OPTION_LOGS );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -15,7 +15,7 @@ namespace WPBridge\Core;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -23,290 +23,275 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class RemoteConfig {
|
||||
|
||||
/**
|
||||
* 远程配置 URL
|
||||
*/
|
||||
const CONFIG_URL = 'https://wpcy.com/api/bridge/commercial-config.json';
|
||||
/**
|
||||
* 远程配置 URL
|
||||
*/
|
||||
const CONFIG_URL = 'https://wpcy.com/api/bridge/commercial-config.json';
|
||||
|
||||
/**
|
||||
* 缓存键名
|
||||
*/
|
||||
const CACHE_KEY = 'wpbridge_remote_config';
|
||||
/**
|
||||
* 缓存键名
|
||||
*/
|
||||
const CACHE_KEY = 'wpbridge_remote_config';
|
||||
|
||||
/**
|
||||
* 缓存时间(秒)- 默认 12 小时
|
||||
*/
|
||||
const CACHE_TTL = 43200;
|
||||
/**
|
||||
* 缓存时间(秒)- 默认 12 小时
|
||||
*/
|
||||
const CACHE_TTL = 43200;
|
||||
|
||||
/**
|
||||
* 单例实例
|
||||
*
|
||||
* @var RemoteConfig|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
/**
|
||||
* 单例实例
|
||||
*
|
||||
* @var RemoteConfig|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* 配置数据
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private $config = null;
|
||||
/**
|
||||
* 配置数据
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private $config = null;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*
|
||||
* @return RemoteConfig
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
/**
|
||||
* 获取单例实例
|
||||
*
|
||||
* @return RemoteConfig
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->load_config();
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->load_config();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置(优先从缓存)
|
||||
*/
|
||||
private function load_config() {
|
||||
// 尝试从缓存加载
|
||||
$cached = get_transient( self::CACHE_KEY );
|
||||
if ( $cached !== false ) {
|
||||
$this->config = $cached;
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 加载配置(优先从缓存)
|
||||
*/
|
||||
private function load_config() {
|
||||
// 尝试从缓存加载
|
||||
$cached = get_transient( self::CACHE_KEY );
|
||||
if ( $cached !== false ) {
|
||||
$this->config = $cached;
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试从远程获取
|
||||
$remote = $this->fetch_remote_config();
|
||||
if ( $remote !== null ) {
|
||||
$this->config = $remote;
|
||||
set_transient( self::CACHE_KEY, $remote, self::CACHE_TTL );
|
||||
return;
|
||||
}
|
||||
// 尝试从远程获取
|
||||
$remote = $this->fetch_remote_config();
|
||||
if ( $remote !== null ) {
|
||||
$this->config = $remote;
|
||||
set_transient( self::CACHE_KEY, $remote, self::CACHE_TTL );
|
||||
return;
|
||||
}
|
||||
|
||||
// 降级到内置配置
|
||||
$this->config = $this->get_builtin_config();
|
||||
}
|
||||
// 降级到内置配置
|
||||
$this->config = $this->get_builtin_config();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从远程获取配置
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function fetch_remote_config() {
|
||||
$response = wp_remote_get(
|
||||
self::CONFIG_URL,
|
||||
array(
|
||||
'timeout' => 10,
|
||||
'headers' => array(
|
||||
'Accept' => 'application/json',
|
||||
),
|
||||
)
|
||||
);
|
||||
/**
|
||||
* 从远程获取配置
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function fetch_remote_config() {
|
||||
$response = wp_remote_get( self::CONFIG_URL, array(
|
||||
'timeout' => 10,
|
||||
'headers' => array(
|
||||
'Accept' => 'application/json',
|
||||
),
|
||||
) );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::warning(
|
||||
'远程配置获取失败',
|
||||
array(
|
||||
'error' => $response->get_error_message(),
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::warning( '远程配置获取失败', array(
|
||||
'error' => $response->get_error_message(),
|
||||
) );
|
||||
return null;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
if ( $code !== 200 ) {
|
||||
Logger::warning(
|
||||
'远程配置响应异常',
|
||||
array(
|
||||
'code' => $code,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
if ( $code !== 200 ) {
|
||||
Logger::warning( '远程配置响应异常', array(
|
||||
'code' => $code,
|
||||
) );
|
||||
return null;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
Logger::warning(
|
||||
'远程配置 JSON 解析失败',
|
||||
array(
|
||||
'error' => json_last_error_msg(),
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
Logger::warning( '远程配置 JSON 解析失败', array(
|
||||
'error' => json_last_error_msg(),
|
||||
) );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证配置结构
|
||||
if ( ! $this->validate_config( $data ) ) {
|
||||
Logger::warning( '远程配置结构无效' );
|
||||
return null;
|
||||
}
|
||||
// 验证配置结构
|
||||
if ( ! $this->validate_config( $data ) ) {
|
||||
Logger::warning( '远程配置结构无效' );
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger::info(
|
||||
'远程配置加载成功',
|
||||
array(
|
||||
'version' => $data['version'] ?? 'unknown',
|
||||
)
|
||||
);
|
||||
Logger::info( '远程配置加载成功', array(
|
||||
'version' => $data['version'] ?? 'unknown',
|
||||
) );
|
||||
|
||||
return $data;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置结构
|
||||
*
|
||||
* @param array $data 配置数据
|
||||
* @return bool
|
||||
*/
|
||||
private function validate_config( $data ) {
|
||||
if ( ! is_array( $data ) ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 验证配置结构
|
||||
*
|
||||
* @param array $data 配置数据
|
||||
* @return bool
|
||||
*/
|
||||
private function validate_config( $data ) {
|
||||
if ( ! is_array( $data ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 必须包含版本号
|
||||
if ( empty( $data['version'] ) ) {
|
||||
return false;
|
||||
}
|
||||
// 必须包含版本号
|
||||
if ( empty( $data['version'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内置配置(降级方案)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_builtin_config() {
|
||||
return array(
|
||||
'version' => '1.0.0-builtin',
|
||||
'updated_at' => '2026-02-04',
|
||||
'commercial_plugins' => array(
|
||||
'elementor-pro',
|
||||
'wordpress-seo-premium',
|
||||
'gravityforms',
|
||||
'advanced-custom-fields-pro',
|
||||
'wp-rocket',
|
||||
'wpforms-pro',
|
||||
'memberpress',
|
||||
'learndash',
|
||||
),
|
||||
'commercial_domains' => array(
|
||||
'codecanyon.net',
|
||||
'themeforest.net',
|
||||
'elegantthemes.com',
|
||||
),
|
||||
'license_keywords' => array(
|
||||
'license_key',
|
||||
'license_status',
|
||||
'activate_license',
|
||||
'deactivate_license',
|
||||
'check_license',
|
||||
),
|
||||
'commercial_frameworks' => array(
|
||||
'EDD_SL_Plugin_Updater',
|
||||
'Freemius',
|
||||
'WC_AM_Client',
|
||||
'Starter_Plugin_Updater',
|
||||
),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取内置配置(降级方案)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_builtin_config() {
|
||||
return array(
|
||||
'version' => '1.0.0-builtin',
|
||||
'updated_at' => '2026-02-04',
|
||||
'commercial_plugins' => array(
|
||||
'elementor-pro',
|
||||
'wordpress-seo-premium',
|
||||
'gravityforms',
|
||||
'advanced-custom-fields-pro',
|
||||
'wp-rocket',
|
||||
'wpforms-pro',
|
||||
'memberpress',
|
||||
'learndash',
|
||||
),
|
||||
'commercial_domains' => array(
|
||||
'codecanyon.net',
|
||||
'themeforest.net',
|
||||
'elegantthemes.com',
|
||||
),
|
||||
'license_keywords' => array(
|
||||
'license_key',
|
||||
'license_status',
|
||||
'activate_license',
|
||||
'deactivate_license',
|
||||
'check_license',
|
||||
),
|
||||
'commercial_frameworks' => array(
|
||||
'EDD_SL_Plugin_Updater',
|
||||
'Freemius',
|
||||
'WC_AM_Client',
|
||||
'Starter_Plugin_Updater',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商业插件列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_commercial_plugins() {
|
||||
return $this->config['commercial_plugins'] ?? array();
|
||||
}
|
||||
/**
|
||||
* 获取商业插件列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_commercial_plugins() {
|
||||
return $this->config['commercial_plugins'] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商业域名列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_commercial_domains() {
|
||||
return $this->config['commercial_domains'] ?? array();
|
||||
}
|
||||
/**
|
||||
* 获取商业域名列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_commercial_domains() {
|
||||
return $this->config['commercial_domains'] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 License 关键词列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_license_keywords() {
|
||||
return $this->config['license_keywords'] ?? array();
|
||||
}
|
||||
/**
|
||||
* 获取 License 关键词列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_license_keywords() {
|
||||
return $this->config['license_keywords'] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商业框架列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_commercial_frameworks() {
|
||||
return $this->config['commercial_frameworks'] ?? array();
|
||||
}
|
||||
/**
|
||||
* 获取商业框架列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_commercial_frameworks() {
|
||||
return $this->config['commercial_frameworks'] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置版本
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_version() {
|
||||
return $this->config['version'] ?? 'unknown';
|
||||
}
|
||||
/**
|
||||
* 获取配置版本
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_version() {
|
||||
return $this->config['version'] ?? 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置更新时间
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_updated_at() {
|
||||
return $this->config['updated_at'] ?? 'unknown';
|
||||
}
|
||||
/**
|
||||
* 获取配置更新时间
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_updated_at() {
|
||||
return $this->config['updated_at'] ?? 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制刷新配置
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function refresh() {
|
||||
delete_transient( self::CACHE_KEY );
|
||||
/**
|
||||
* 强制刷新配置
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function refresh() {
|
||||
delete_transient( self::CACHE_KEY );
|
||||
|
||||
$remote = $this->fetch_remote_config();
|
||||
if ( $remote !== null ) {
|
||||
$this->config = $remote;
|
||||
set_transient( self::CACHE_KEY, $remote, self::CACHE_TTL );
|
||||
return true;
|
||||
}
|
||||
$remote = $this->fetch_remote_config();
|
||||
if ( $remote !== null ) {
|
||||
$this->config = $remote;
|
||||
set_transient( self::CACHE_KEY, $remote, self::CACHE_TTL );
|
||||
return true;
|
||||
}
|
||||
|
||||
// 刷新失败,保持当前配置
|
||||
return false;
|
||||
}
|
||||
// 刷新失败,保持当前配置
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否使用内置配置
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_builtin() {
|
||||
return strpos( $this->get_version(), 'builtin' ) !== false;
|
||||
}
|
||||
/**
|
||||
* 检查是否使用内置配置
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_builtin() {
|
||||
return strpos( $this->get_version(), 'builtin' ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整配置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all() {
|
||||
return $this->config;
|
||||
}
|
||||
/**
|
||||
* 获取完整配置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all() {
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\Core;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,346 +17,334 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class Settings {
|
||||
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_SOURCES = 'wpbridge_sources';
|
||||
const OPTION_SETTINGS = 'wpbridge_settings';
|
||||
const OPTION_AI_SETTINGS = 'wpbridge_ai_settings';
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_SOURCES = 'wpbridge_sources';
|
||||
const OPTION_SETTINGS = 'wpbridge_settings';
|
||||
const OPTION_AI_SETTINGS = 'wpbridge_ai_settings';
|
||||
|
||||
/**
|
||||
* 默认设置
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $defaults = array(
|
||||
'debug_mode' => false,
|
||||
'cache_ttl' => 43200, // 12 小时
|
||||
'request_timeout' => 10, // 秒
|
||||
'fallback_enabled' => true,
|
||||
);
|
||||
/**
|
||||
* 默认设置
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $defaults = [
|
||||
'debug_mode' => false,
|
||||
'cache_ttl' => 43200, // 12 小时
|
||||
'request_timeout' => 10, // 秒
|
||||
'fallback_enabled' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* 缓存的设置
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $cached_settings = null;
|
||||
/**
|
||||
* 缓存的设置
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $cached_settings = null;
|
||||
|
||||
/**
|
||||
* 缓存的更新源
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $cached_sources = null;
|
||||
/**
|
||||
* 缓存的更新源
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $cached_sources = null;
|
||||
|
||||
/**
|
||||
* 初始化默认设置
|
||||
*/
|
||||
public function init_defaults(): void {
|
||||
// 初始化基础设置
|
||||
if ( false === get_option( self::OPTION_SETTINGS ) ) {
|
||||
update_option( self::OPTION_SETTINGS, $this->defaults );
|
||||
}
|
||||
/**
|
||||
* 初始化默认设置
|
||||
*/
|
||||
public function init_defaults(): void {
|
||||
// 初始化基础设置
|
||||
if ( false === get_option( self::OPTION_SETTINGS ) ) {
|
||||
update_option( self::OPTION_SETTINGS, $this->defaults );
|
||||
}
|
||||
|
||||
// 初始化更新源(包含预置源)
|
||||
if ( false === get_option( self::OPTION_SOURCES ) ) {
|
||||
update_option( self::OPTION_SOURCES, $this->get_preset_sources() );
|
||||
}
|
||||
// 初始化更新源(包含预置源)
|
||||
if ( false === get_option( self::OPTION_SOURCES ) ) {
|
||||
update_option( self::OPTION_SOURCES, $this->get_preset_sources() );
|
||||
}
|
||||
|
||||
// 初始化 AI 设置
|
||||
if ( false === get_option( self::OPTION_AI_SETTINGS ) ) {
|
||||
update_option(
|
||||
self::OPTION_AI_SETTINGS,
|
||||
array(
|
||||
'enabled' => false,
|
||||
'mode' => 'disabled',
|
||||
'whitelist' => array( 'api.openai.com', 'api.anthropic.com' ),
|
||||
'custom_endpoint' => '',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
// 初始化 AI 设置
|
||||
if ( false === get_option( self::OPTION_AI_SETTINGS ) ) {
|
||||
update_option( self::OPTION_AI_SETTINGS, [
|
||||
'enabled' => false,
|
||||
'mode' => 'disabled',
|
||||
'whitelist' => [ 'api.openai.com', 'api.anthropic.com' ],
|
||||
'custom_endpoint' => '',
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预置更新源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_preset_sources(): array {
|
||||
return array(
|
||||
array(
|
||||
'id' => 'wenpai-open',
|
||||
'name' => __( '文派开源更新源', 'wpbridge' ),
|
||||
'type' => 'json',
|
||||
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
|
||||
'slug' => '',
|
||||
'item_type' => 'plugin',
|
||||
'auth_token' => '',
|
||||
'enabled' => true,
|
||||
'priority' => 10,
|
||||
'is_preset' => true,
|
||||
'metadata' => array(),
|
||||
),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取预置更新源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_preset_sources(): array {
|
||||
return [
|
||||
[
|
||||
'id' => 'wenpai-open',
|
||||
'name' => __( '文派开源更新源', 'wpbridge' ),
|
||||
'type' => 'json',
|
||||
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
|
||||
'slug' => '',
|
||||
'item_type' => 'plugin',
|
||||
'auth_token' => '',
|
||||
'enabled' => true,
|
||||
'priority' => 10,
|
||||
'is_preset' => true,
|
||||
'metadata' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有设置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->cached_settings ) {
|
||||
$this->cached_settings = wp_parse_args(
|
||||
get_option( self::OPTION_SETTINGS, array() ),
|
||||
$this->defaults
|
||||
);
|
||||
}
|
||||
return $this->cached_settings;
|
||||
}
|
||||
/**
|
||||
* 获取所有设置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->cached_settings ) {
|
||||
$this->cached_settings = wp_parse_args(
|
||||
get_option( self::OPTION_SETTINGS, [] ),
|
||||
$this->defaults
|
||||
);
|
||||
}
|
||||
return $this->cached_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个设置
|
||||
*
|
||||
* @param string $key 设置键
|
||||
* @param mixed $default 默认值
|
||||
* @return mixed
|
||||
*/
|
||||
public function get( string $key, $default = null ) {
|
||||
$settings = $this->get_all();
|
||||
return $settings[ $key ] ?? $default;
|
||||
}
|
||||
/**
|
||||
* 获取单个设置
|
||||
*
|
||||
* @param string $key 设置键
|
||||
* @param mixed $default 默认值
|
||||
* @return mixed
|
||||
*/
|
||||
public function get( string $key, $default = null ) {
|
||||
$settings = $this->get_all();
|
||||
return $settings[ $key ] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设置
|
||||
*
|
||||
* @param string $key 设置键
|
||||
* @param mixed $value 设置值
|
||||
* @return bool
|
||||
*/
|
||||
public function set( string $key, $value ): bool {
|
||||
$settings = $this->get_all();
|
||||
$settings[ $key ] = $value;
|
||||
/**
|
||||
* 更新设置
|
||||
*
|
||||
* @param string $key 设置键
|
||||
* @param mixed $value 设置值
|
||||
* @return bool
|
||||
*/
|
||||
public function set( string $key, $value ): bool {
|
||||
$settings = $this->get_all();
|
||||
$settings[ $key ] = $value;
|
||||
|
||||
$this->cached_settings = $settings;
|
||||
return update_option( self::OPTION_SETTINGS, $settings );
|
||||
}
|
||||
$this->cached_settings = $settings;
|
||||
return update_option( self::OPTION_SETTINGS, $settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新设置
|
||||
*
|
||||
* @param array $settings 设置数组
|
||||
* @return bool
|
||||
*/
|
||||
public function update( array $settings ): bool {
|
||||
$current = $this->get_all();
|
||||
$merged = wp_parse_args( $settings, $current );
|
||||
/**
|
||||
* 批量更新设置
|
||||
*
|
||||
* @param array $settings 设置数组
|
||||
* @return bool
|
||||
*/
|
||||
public function update( array $settings ): bool {
|
||||
$current = $this->get_all();
|
||||
$merged = wp_parse_args( $settings, $current );
|
||||
|
||||
$this->cached_settings = $merged;
|
||||
return update_option( self::OPTION_SETTINGS, $merged );
|
||||
}
|
||||
$this->cached_settings = $merged;
|
||||
return update_option( self::OPTION_SETTINGS, $merged );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有更新源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_sources(): array {
|
||||
if ( null === $this->cached_sources ) {
|
||||
$this->cached_sources = get_option( self::OPTION_SOURCES, array() );
|
||||
}
|
||||
return $this->cached_sources;
|
||||
}
|
||||
/**
|
||||
* 获取所有更新源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_sources(): array {
|
||||
if ( null === $this->cached_sources ) {
|
||||
$this->cached_sources = get_option( self::OPTION_SOURCES, [] );
|
||||
}
|
||||
return $this->cached_sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的更新源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_enabled_sources(): array {
|
||||
$sources = $this->get_sources();
|
||||
return array_filter(
|
||||
$sources,
|
||||
function ( $source ) {
|
||||
return ! empty( $source['enabled'] );
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取启用的更新源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_enabled_sources(): array {
|
||||
$sources = $this->get_sources();
|
||||
return array_filter( $sources, function( $source ) {
|
||||
return ! empty( $source['enabled'] );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个更新源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_source( string $id ): ?array {
|
||||
$sources = $this->get_sources();
|
||||
foreach ( $sources as $source ) {
|
||||
if ( $source['id'] === $id ) {
|
||||
return $source;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 获取单个更新源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_source( string $id ): ?array {
|
||||
$sources = $this->get_sources();
|
||||
foreach ( $sources as $source ) {
|
||||
if ( $source['id'] === $id ) {
|
||||
return $source;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加更新源
|
||||
*
|
||||
* @param array $source 源数据
|
||||
* @return bool
|
||||
*/
|
||||
public function add_source( array $source ): bool {
|
||||
$sources = $this->get_sources();
|
||||
/**
|
||||
* 添加更新源
|
||||
*
|
||||
* @param array $source 源数据
|
||||
* @return bool
|
||||
*/
|
||||
public function add_source( array $source ): bool {
|
||||
$sources = $this->get_sources();
|
||||
|
||||
// 生成唯一 ID
|
||||
if ( empty( $source['id'] ) ) {
|
||||
$source['id'] = 'source_' . wp_generate_uuid4();
|
||||
}
|
||||
// 生成唯一 ID
|
||||
if ( empty( $source['id'] ) ) {
|
||||
$source['id'] = 'source_' . wp_generate_uuid4();
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
$source = wp_parse_args(
|
||||
$source,
|
||||
array(
|
||||
'name' => '',
|
||||
'type' => 'json',
|
||||
'api_url' => '',
|
||||
'slug' => '',
|
||||
'item_type' => 'plugin',
|
||||
'auth_token' => '',
|
||||
'enabled' => true,
|
||||
'priority' => 50,
|
||||
'is_preset' => false,
|
||||
'metadata' => array(),
|
||||
)
|
||||
);
|
||||
// 设置默认值
|
||||
$source = wp_parse_args( $source, [
|
||||
'name' => '',
|
||||
'type' => 'json',
|
||||
'api_url' => '',
|
||||
'slug' => '',
|
||||
'item_type' => 'plugin',
|
||||
'auth_token' => '',
|
||||
'enabled' => true,
|
||||
'priority' => 50,
|
||||
'is_preset' => false,
|
||||
'metadata' => [],
|
||||
] );
|
||||
|
||||
$sources[] = $source;
|
||||
$sources[] = $source;
|
||||
|
||||
$this->cached_sources = $sources;
|
||||
return update_option( self::OPTION_SOURCES, $sources );
|
||||
}
|
||||
$this->cached_sources = $sources;
|
||||
return update_option( self::OPTION_SOURCES, $sources );
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新更新源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @param array $data 更新数据
|
||||
* @return bool
|
||||
*/
|
||||
public function update_source( string $id, array $data ): bool {
|
||||
$sources = $this->get_sources();
|
||||
/**
|
||||
* 更新更新源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @param array $data 更新数据
|
||||
* @return bool
|
||||
*/
|
||||
public function update_source( string $id, array $data ): bool {
|
||||
$sources = $this->get_sources();
|
||||
|
||||
foreach ( $sources as $index => $source ) {
|
||||
if ( $source['id'] === $id ) {
|
||||
$sources[ $index ] = wp_parse_args( $data, $source );
|
||||
$this->cached_sources = $sources;
|
||||
return update_option( self::OPTION_SOURCES, $sources );
|
||||
}
|
||||
}
|
||||
foreach ( $sources as $index => $source ) {
|
||||
if ( $source['id'] === $id ) {
|
||||
$sources[ $index ] = wp_parse_args( $data, $source );
|
||||
$this->cached_sources = $sources;
|
||||
return update_option( self::OPTION_SOURCES, $sources );
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除更新源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_source( string $id ): bool {
|
||||
$sources = $this->get_sources();
|
||||
/**
|
||||
* 删除更新源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_source( string $id ): bool {
|
||||
$sources = $this->get_sources();
|
||||
|
||||
foreach ( $sources as $index => $source ) {
|
||||
if ( $source['id'] === $id ) {
|
||||
// 不允许删除预置源
|
||||
if ( ! empty( $source['is_preset'] ) ) {
|
||||
return false;
|
||||
}
|
||||
foreach ( $sources as $index => $source ) {
|
||||
if ( $source['id'] === $id ) {
|
||||
// 不允许删除预置源
|
||||
if ( ! empty( $source['is_preset'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset( $sources[ $index ] );
|
||||
$sources = array_values( $sources ); // 重新索引
|
||||
unset( $sources[ $index ] );
|
||||
$sources = array_values( $sources ); // 重新索引
|
||||
|
||||
$this->cached_sources = $sources;
|
||||
return update_option( self::OPTION_SOURCES, $sources );
|
||||
}
|
||||
}
|
||||
$this->cached_sources = $sources;
|
||||
return update_option( self::OPTION_SOURCES, $sources );
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用更新源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @param bool $enabled 是否启用
|
||||
* @return bool
|
||||
*/
|
||||
public function toggle_source( string $id, bool $enabled ): bool {
|
||||
return $this->update_source( $id, array( 'enabled' => $enabled ) );
|
||||
}
|
||||
/**
|
||||
* 启用/禁用更新源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @param bool $enabled 是否启用
|
||||
* @return bool
|
||||
*/
|
||||
public function toggle_source( string $id, bool $enabled ): bool {
|
||||
return $this->update_source( $id, [ 'enabled' => $enabled ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 设置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_ai_settings(): array {
|
||||
return get_option(
|
||||
self::OPTION_AI_SETTINGS,
|
||||
array(
|
||||
'enabled' => false,
|
||||
'mode' => 'disabled',
|
||||
'whitelist' => array( 'api.openai.com', 'api.anthropic.com' ),
|
||||
'custom_endpoint' => '',
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取 AI 设置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_ai_settings(): array {
|
||||
return get_option( self::OPTION_AI_SETTINGS, [
|
||||
'enabled' => false,
|
||||
'mode' => 'disabled',
|
||||
'whitelist' => [ 'api.openai.com', 'api.anthropic.com' ],
|
||||
'custom_endpoint' => '',
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 AI 设置
|
||||
*
|
||||
* @param array $settings AI 设置
|
||||
* @return bool
|
||||
*/
|
||||
public function update_ai_settings( array $settings ): bool {
|
||||
$current = $this->get_ai_settings();
|
||||
$merged = wp_parse_args( $settings, $current );
|
||||
return update_option( self::OPTION_AI_SETTINGS, $merged );
|
||||
}
|
||||
/**
|
||||
* 更新 AI 设置
|
||||
*
|
||||
* @param array $settings AI 设置
|
||||
* @return bool
|
||||
*/
|
||||
public function update_ai_settings( array $settings ): bool {
|
||||
$current = $this->get_ai_settings();
|
||||
$merged = wp_parse_args( $settings, $current );
|
||||
return update_option( self::OPTION_AI_SETTINGS, $merged );
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用调试模式
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_debug(): bool {
|
||||
return (bool) $this->get( 'debug_mode', false );
|
||||
}
|
||||
/**
|
||||
* 是否启用调试模式
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_debug(): bool {
|
||||
return (bool) $this->get( 'debug_mode', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存 TTL
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_cache_ttl(): int {
|
||||
return (int) $this->get( 'cache_ttl', 43200 );
|
||||
}
|
||||
/**
|
||||
* 获取缓存 TTL
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_cache_ttl(): int {
|
||||
return (int) $this->get( 'cache_ttl', 43200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求超时时间
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_request_timeout(): int {
|
||||
return (int) $this->get( 'request_timeout', 10 );
|
||||
}
|
||||
/**
|
||||
* 获取请求超时时间
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_request_timeout(): int {
|
||||
return (int) $this->get( 'request_timeout', 10 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除设置缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->cached_settings = null;
|
||||
$this->cached_sources = null;
|
||||
}
|
||||
/**
|
||||
* 清除设置缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->cached_settings = null;
|
||||
$this->cached_sources = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Cache\HealthChecker;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,288 +19,288 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class SiteHealth {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->init_hooks();
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 添加健康检查测试
|
||||
add_filter( 'site_status_tests', array( $this, 'add_tests' ) );
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 添加健康检查测试
|
||||
add_filter( 'site_status_tests', [ $this, 'add_tests' ] );
|
||||
|
||||
// 添加调试信息
|
||||
add_filter( 'debug_information', array( $this, 'add_debug_info' ) );
|
||||
}
|
||||
// 添加调试信息
|
||||
add_filter( 'debug_information', [ $this, 'add_debug_info' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加健康检查测试
|
||||
*
|
||||
* @param array $tests 测试列表
|
||||
* @return array
|
||||
*/
|
||||
public function add_tests( array $tests ): array {
|
||||
$tests['direct']['wpbridge_sources'] = array(
|
||||
'label' => __( 'WPBridge 更新源状态', 'wpbridge' ),
|
||||
'test' => array( $this, 'test_sources' ),
|
||||
);
|
||||
/**
|
||||
* 添加健康检查测试
|
||||
*
|
||||
* @param array $tests 测试列表
|
||||
* @return array
|
||||
*/
|
||||
public function add_tests( array $tests ): array {
|
||||
$tests['direct']['wpbridge_sources'] = [
|
||||
'label' => __( 'WPBridge 更新源状态', 'wpbridge' ),
|
||||
'test' => [ $this, 'test_sources' ],
|
||||
];
|
||||
|
||||
$tests['direct']['wpbridge_config'] = array(
|
||||
'label' => __( 'WPBridge 配置检查', 'wpbridge' ),
|
||||
'test' => array( $this, 'test_config' ),
|
||||
);
|
||||
$tests['direct']['wpbridge_config'] = [
|
||||
'label' => __( 'WPBridge 配置检查', 'wpbridge' ),
|
||||
'test' => [ $this, 'test_config' ],
|
||||
];
|
||||
|
||||
return $tests;
|
||||
}
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试更新源状态
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function test_sources(): array {
|
||||
$sources = $this->settings->get_enabled_sources();
|
||||
$health_checker = new HealthChecker( $this->settings );
|
||||
/**
|
||||
* 测试更新源状态
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function test_sources(): array {
|
||||
$sources = $this->settings->get_enabled_sources();
|
||||
$health_checker = new HealthChecker( $this->settings );
|
||||
|
||||
$healthy = 0;
|
||||
$degraded = 0;
|
||||
$failed = 0;
|
||||
$failed_sources = array();
|
||||
$healthy = 0;
|
||||
$degraded = 0;
|
||||
$failed = 0;
|
||||
$failed_sources = [];
|
||||
|
||||
foreach ( $sources as $source ) {
|
||||
$status = $health_checker->check( $source );
|
||||
foreach ( $sources as $source ) {
|
||||
$status = $health_checker->check( $source );
|
||||
|
||||
if ( $status['status'] === 'healthy' ) {
|
||||
++$healthy;
|
||||
} elseif ( $status['status'] === 'degraded' ) {
|
||||
++$degraded;
|
||||
} else {
|
||||
++$failed;
|
||||
$failed_sources[] = $source['name'];
|
||||
}
|
||||
}
|
||||
if ( $status['status'] === 'healthy' ) {
|
||||
$healthy++;
|
||||
} elseif ( $status['status'] === 'degraded' ) {
|
||||
$degraded++;
|
||||
} else {
|
||||
$failed++;
|
||||
$failed_sources[] = $source['name'];
|
||||
}
|
||||
}
|
||||
|
||||
$total = count( $sources );
|
||||
$total = count( $sources );
|
||||
|
||||
if ( $total === 0 ) {
|
||||
return array(
|
||||
'label' => __( 'WPBridge: 未配置更新源', 'wpbridge' ),
|
||||
'status' => 'recommended',
|
||||
'badge' => array(
|
||||
'label' => __( '推荐', 'wpbridge' ),
|
||||
'color' => 'orange',
|
||||
),
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( '您尚未配置任何自定义更新源。如果您需要使用自定义更新源,请在 WPBridge 设置中添加。', 'wpbridge' )
|
||||
),
|
||||
'actions' => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
admin_url( 'admin.php?page=wpbridge' ),
|
||||
__( '配置更新源', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_sources',
|
||||
);
|
||||
}
|
||||
if ( $total === 0 ) {
|
||||
return [
|
||||
'label' => __( 'WPBridge: 未配置更新源', 'wpbridge' ),
|
||||
'status' => 'recommended',
|
||||
'badge' => [
|
||||
'label' => __( '推荐', 'wpbridge' ),
|
||||
'color' => 'orange',
|
||||
],
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( '您尚未配置任何自定义更新源。如果您需要使用自定义更新源,请在 WPBridge 设置中添加。', 'wpbridge' )
|
||||
),
|
||||
'actions' => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
admin_url( 'admin.php?page=wpbridge' ),
|
||||
__( '配置更新源', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_sources',
|
||||
];
|
||||
}
|
||||
|
||||
if ( $failed > 0 ) {
|
||||
return array(
|
||||
'label' => sprintf(
|
||||
__( 'WPBridge: %d 个更新源不可用', 'wpbridge' ),
|
||||
$failed
|
||||
),
|
||||
'status' => 'critical',
|
||||
'badge' => array(
|
||||
'label' => __( '错误', 'wpbridge' ),
|
||||
'color' => 'red',
|
||||
),
|
||||
'description' => sprintf(
|
||||
'<p>%s</p><p>%s: %s</p>',
|
||||
__( '部分更新源无法连接,这可能导致插件/主题无法正常更新。', 'wpbridge' ),
|
||||
__( '不可用的更新源', 'wpbridge' ),
|
||||
implode( ', ', $failed_sources )
|
||||
),
|
||||
'actions' => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
admin_url( 'admin.php?page=wpbridge&tab=diagnostics' ),
|
||||
__( '查看诊断', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_sources',
|
||||
);
|
||||
}
|
||||
if ( $failed > 0 ) {
|
||||
return [
|
||||
'label' => sprintf(
|
||||
__( 'WPBridge: %d 个更新源不可用', 'wpbridge' ),
|
||||
$failed
|
||||
),
|
||||
'status' => 'critical',
|
||||
'badge' => [
|
||||
'label' => __( '错误', 'wpbridge' ),
|
||||
'color' => 'red',
|
||||
],
|
||||
'description' => sprintf(
|
||||
'<p>%s</p><p>%s: %s</p>',
|
||||
__( '部分更新源无法连接,这可能导致插件/主题无法正常更新。', 'wpbridge' ),
|
||||
__( '不可用的更新源', 'wpbridge' ),
|
||||
implode( ', ', $failed_sources )
|
||||
),
|
||||
'actions' => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
admin_url( 'admin.php?page=wpbridge&tab=diagnostics' ),
|
||||
__( '查看诊断', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_sources',
|
||||
];
|
||||
}
|
||||
|
||||
if ( $degraded > 0 ) {
|
||||
return array(
|
||||
'label' => sprintf(
|
||||
__( 'WPBridge: %d 个更新源响应较慢', 'wpbridge' ),
|
||||
$degraded
|
||||
),
|
||||
'status' => 'recommended',
|
||||
'badge' => array(
|
||||
'label' => __( '警告', 'wpbridge' ),
|
||||
'color' => 'orange',
|
||||
),
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( '部分更新源响应时间较长,可能影响更新检查速度。', 'wpbridge' )
|
||||
),
|
||||
'actions' => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
admin_url( 'admin.php?page=wpbridge&tab=diagnostics' ),
|
||||
__( '查看诊断', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_sources',
|
||||
);
|
||||
}
|
||||
if ( $degraded > 0 ) {
|
||||
return [
|
||||
'label' => sprintf(
|
||||
__( 'WPBridge: %d 个更新源响应较慢', 'wpbridge' ),
|
||||
$degraded
|
||||
),
|
||||
'status' => 'recommended',
|
||||
'badge' => [
|
||||
'label' => __( '警告', 'wpbridge' ),
|
||||
'color' => 'orange',
|
||||
],
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( '部分更新源响应时间较长,可能影响更新检查速度。', 'wpbridge' )
|
||||
),
|
||||
'actions' => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
admin_url( 'admin.php?page=wpbridge&tab=diagnostics' ),
|
||||
__( '查看诊断', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_sources',
|
||||
];
|
||||
}
|
||||
|
||||
return array(
|
||||
'label' => sprintf(
|
||||
__( 'WPBridge: 所有 %d 个更新源正常', 'wpbridge' ),
|
||||
$healthy
|
||||
),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => __( '正常', 'wpbridge' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( '所有配置的更新源都可以正常连接。', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_sources',
|
||||
);
|
||||
}
|
||||
return [
|
||||
'label' => sprintf(
|
||||
__( 'WPBridge: 所有 %d 个更新源正常', 'wpbridge' ),
|
||||
$healthy
|
||||
),
|
||||
'status' => 'good',
|
||||
'badge' => [
|
||||
'label' => __( '正常', 'wpbridge' ),
|
||||
'color' => 'green',
|
||||
],
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( '所有配置的更新源都可以正常连接。', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_sources',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试配置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function test_config(): array {
|
||||
$issues = array();
|
||||
/**
|
||||
* 测试配置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function test_config(): array {
|
||||
$issues = [];
|
||||
|
||||
// 检查调试模式
|
||||
if ( $this->settings->is_debug() ) {
|
||||
$issues[] = __( '调试模式已启用,建议在生产环境中关闭', 'wpbridge' );
|
||||
}
|
||||
// 检查调试模式
|
||||
if ( $this->settings->is_debug() ) {
|
||||
$issues[] = __( '调试模式已启用,建议在生产环境中关闭', 'wpbridge' );
|
||||
}
|
||||
|
||||
// 检查缓存时间
|
||||
$cache_ttl = $this->settings->get_cache_ttl();
|
||||
if ( $cache_ttl < 3600 ) {
|
||||
$issues[] = __( '缓存时间设置过短,可能导致频繁请求', 'wpbridge' );
|
||||
}
|
||||
// 检查缓存时间
|
||||
$cache_ttl = $this->settings->get_cache_ttl();
|
||||
if ( $cache_ttl < 3600 ) {
|
||||
$issues[] = __( '缓存时间设置过短,可能导致频繁请求', 'wpbridge' );
|
||||
}
|
||||
|
||||
// 检查备份功能
|
||||
if ( ! $this->settings->get( 'backup_enabled', true ) ) {
|
||||
$issues[] = __( '更新前备份已禁用,建议启用以便回滚', 'wpbridge' );
|
||||
}
|
||||
// 检查备份功能
|
||||
if ( ! $this->settings->get( 'backup_enabled', true ) ) {
|
||||
$issues[] = __( '更新前备份已禁用,建议启用以便回滚', 'wpbridge' );
|
||||
}
|
||||
|
||||
// 检查 ZipArchive
|
||||
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||
$issues[] = __( 'PHP ZipArchive 扩展未安装,备份功能将不可用', 'wpbridge' );
|
||||
}
|
||||
// 检查 ZipArchive
|
||||
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||
$issues[] = __( 'PHP ZipArchive 扩展未安装,备份功能将不可用', 'wpbridge' );
|
||||
}
|
||||
|
||||
if ( ! empty( $issues ) ) {
|
||||
return array(
|
||||
'label' => __( 'WPBridge: 配置需要优化', 'wpbridge' ),
|
||||
'status' => 'recommended',
|
||||
'badge' => array(
|
||||
'label' => __( '建议', 'wpbridge' ),
|
||||
'color' => 'orange',
|
||||
),
|
||||
'description' => sprintf(
|
||||
'<p>%s</p><ul><li>%s</li></ul>',
|
||||
__( '发现以下配置问题:', 'wpbridge' ),
|
||||
implode( '</li><li>', $issues )
|
||||
),
|
||||
'actions' => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
admin_url( 'admin.php?page=wpbridge&tab=settings' ),
|
||||
__( '调整设置', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_config',
|
||||
);
|
||||
}
|
||||
if ( ! empty( $issues ) ) {
|
||||
return [
|
||||
'label' => __( 'WPBridge: 配置需要优化', 'wpbridge' ),
|
||||
'status' => 'recommended',
|
||||
'badge' => [
|
||||
'label' => __( '建议', 'wpbridge' ),
|
||||
'color' => 'orange',
|
||||
],
|
||||
'description' => sprintf(
|
||||
'<p>%s</p><ul><li>%s</li></ul>',
|
||||
__( '发现以下配置问题:', 'wpbridge' ),
|
||||
implode( '</li><li>', $issues )
|
||||
),
|
||||
'actions' => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
admin_url( 'admin.php?page=wpbridge&tab=settings' ),
|
||||
__( '调整设置', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_config',
|
||||
];
|
||||
}
|
||||
|
||||
return array(
|
||||
'label' => __( 'WPBridge: 配置正常', 'wpbridge' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => __( '正常', 'wpbridge' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( 'WPBridge 配置正确,所有功能正常运行。', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_config',
|
||||
);
|
||||
}
|
||||
return [
|
||||
'label' => __( 'WPBridge: 配置正常', 'wpbridge' ),
|
||||
'status' => 'good',
|
||||
'badge' => [
|
||||
'label' => __( '正常', 'wpbridge' ),
|
||||
'color' => 'green',
|
||||
],
|
||||
'description' => sprintf(
|
||||
'<p>%s</p>',
|
||||
__( 'WPBridge 配置正确,所有功能正常运行。', 'wpbridge' )
|
||||
),
|
||||
'test' => 'wpbridge_config',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加调试信息
|
||||
*
|
||||
* @param array $info 调试信息
|
||||
* @return array
|
||||
*/
|
||||
public function add_debug_info( array $info ): array {
|
||||
$sources = $this->settings->get_sources();
|
||||
$enabled_sources = $this->settings->get_enabled_sources();
|
||||
$version_lock = VersionLock::get_instance();
|
||||
$backup_manager = BackupManager::get_instance();
|
||||
/**
|
||||
* 添加调试信息
|
||||
*
|
||||
* @param array $info 调试信息
|
||||
* @return array
|
||||
*/
|
||||
public function add_debug_info( array $info ): array {
|
||||
$sources = $this->settings->get_sources();
|
||||
$enabled_sources = $this->settings->get_enabled_sources();
|
||||
$version_lock = VersionLock::get_instance();
|
||||
$backup_manager = BackupManager::get_instance();
|
||||
|
||||
$info['wpbridge'] = array(
|
||||
'label' => 'WPBridge',
|
||||
'fields' => array(
|
||||
'version' => array(
|
||||
'label' => __( '版本', 'wpbridge' ),
|
||||
'value' => WPBRIDGE_VERSION,
|
||||
),
|
||||
'total_sources' => array(
|
||||
'label' => __( '总更新源数', 'wpbridge' ),
|
||||
'value' => count( $sources ),
|
||||
),
|
||||
'enabled_sources' => array(
|
||||
'label' => __( '已启用更新源', 'wpbridge' ),
|
||||
'value' => count( $enabled_sources ),
|
||||
),
|
||||
'locked_items' => array(
|
||||
'label' => __( '已锁定项目数', 'wpbridge' ),
|
||||
'value' => count( $version_lock->get_all() ),
|
||||
),
|
||||
'backup_size' => array(
|
||||
'label' => __( '备份总大小', 'wpbridge' ),
|
||||
'value' => size_format( $backup_manager->get_total_size() ),
|
||||
),
|
||||
'debug_mode' => array(
|
||||
'label' => __( '调试模式', 'wpbridge' ),
|
||||
'value' => $this->settings->is_debug() ? __( '已启用', 'wpbridge' ) : __( '已禁用', 'wpbridge' ),
|
||||
),
|
||||
'cache_ttl' => array(
|
||||
'label' => __( '缓存时间', 'wpbridge' ),
|
||||
'value' => human_time_diff( 0, $this->settings->get_cache_ttl() ),
|
||||
),
|
||||
'backup_enabled' => array(
|
||||
'label' => __( '更新前备份', 'wpbridge' ),
|
||||
'value' => $this->settings->get( 'backup_enabled', true ) ? __( '已启用', 'wpbridge' ) : __( '已禁用', 'wpbridge' ),
|
||||
),
|
||||
),
|
||||
);
|
||||
$info['wpbridge'] = [
|
||||
'label' => 'WPBridge',
|
||||
'fields' => [
|
||||
'version' => [
|
||||
'label' => __( '版本', 'wpbridge' ),
|
||||
'value' => WPBRIDGE_VERSION,
|
||||
],
|
||||
'total_sources' => [
|
||||
'label' => __( '总更新源数', 'wpbridge' ),
|
||||
'value' => count( $sources ),
|
||||
],
|
||||
'enabled_sources' => [
|
||||
'label' => __( '已启用更新源', 'wpbridge' ),
|
||||
'value' => count( $enabled_sources ),
|
||||
],
|
||||
'locked_items' => [
|
||||
'label' => __( '已锁定项目数', 'wpbridge' ),
|
||||
'value' => count( $version_lock->get_all() ),
|
||||
],
|
||||
'backup_size' => [
|
||||
'label' => __( '备份总大小', 'wpbridge' ),
|
||||
'value' => size_format( $backup_manager->get_total_size() ),
|
||||
],
|
||||
'debug_mode' => [
|
||||
'label' => __( '调试模式', 'wpbridge' ),
|
||||
'value' => $this->settings->is_debug() ? __( '已启用', 'wpbridge' ) : __( '已禁用', 'wpbridge' ),
|
||||
],
|
||||
'cache_ttl' => [
|
||||
'label' => __( '缓存时间', 'wpbridge' ),
|
||||
'value' => human_time_diff( 0, $this->settings->get_cache_ttl() ),
|
||||
],
|
||||
'backup_enabled' => [
|
||||
'label' => __( '更新前备份', 'wpbridge' ),
|
||||
'value' => $this->settings->get( 'backup_enabled', true ) ? __( '已启用', 'wpbridge' ) : __( '已禁用', 'wpbridge' ),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace WPBridge\Core;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,344 +22,341 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class SourceRegistry {
|
||||
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_source_registry';
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_source_registry';
|
||||
|
||||
/**
|
||||
* 源类型枚举
|
||||
*/
|
||||
const TYPE_WPORG = 'wporg';
|
||||
const TYPE_FAIR = 'fair';
|
||||
const TYPE_CUSTOM = 'custom';
|
||||
const TYPE_GIT = 'git';
|
||||
const TYPE_MIRROR = 'mirror';
|
||||
const TYPE_JSON = 'json';
|
||||
const TYPE_ARKPRESS = 'arkpress';
|
||||
/**
|
||||
* 源类型枚举
|
||||
*/
|
||||
const TYPE_WPORG = 'wporg';
|
||||
const TYPE_FAIR = 'fair';
|
||||
const TYPE_CUSTOM = 'custom';
|
||||
const TYPE_GIT = 'git';
|
||||
const TYPE_MIRROR = 'mirror';
|
||||
const TYPE_JSON = 'json';
|
||||
const TYPE_ARKPRESS = 'arkpress';
|
||||
|
||||
/**
|
||||
* 签名方案
|
||||
*/
|
||||
const SIGNATURE_NONE = 'none';
|
||||
const SIGNATURE_ED25519 = 'ed25519';
|
||||
/**
|
||||
* 签名方案
|
||||
*/
|
||||
const SIGNATURE_NONE = 'none';
|
||||
const SIGNATURE_ED25519 = 'ed25519';
|
||||
|
||||
/**
|
||||
* 认证类型
|
||||
*/
|
||||
const AUTH_NONE = 'none';
|
||||
const AUTH_BASIC = 'basic';
|
||||
const AUTH_BEARER = 'bearer';
|
||||
const AUTH_TOKEN = 'token';
|
||||
/**
|
||||
* 认证类型
|
||||
*/
|
||||
const AUTH_NONE = 'none';
|
||||
const AUTH_BASIC = 'basic';
|
||||
const AUTH_BEARER = 'bearer';
|
||||
const AUTH_TOKEN = 'token';
|
||||
|
||||
/**
|
||||
* 缓存的源列表
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $cached_sources = null;
|
||||
/**
|
||||
* 缓存的源列表
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $cached_sources = null;
|
||||
|
||||
/**
|
||||
* 获取所有源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->cached_sources ) {
|
||||
$this->cached_sources = get_option( self::OPTION_NAME, array() );
|
||||
$this->ensure_preset_sources();
|
||||
}
|
||||
return $this->cached_sources;
|
||||
}
|
||||
/**
|
||||
* 获取所有源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->cached_sources ) {
|
||||
$this->cached_sources = get_option( self::OPTION_NAME, [] );
|
||||
$this->ensure_preset_sources();
|
||||
}
|
||||
return $this->cached_sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_enabled(): array {
|
||||
return array_filter( $this->get_all(), fn( $s ) => ! empty( $s['enabled'] ) );
|
||||
}
|
||||
/**
|
||||
* 获取启用的源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_enabled(): array {
|
||||
return array_filter( $this->get_all(), fn( $s ) => ! empty( $s['enabled'] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 按类型获取源
|
||||
*
|
||||
* @param string $type 源类型
|
||||
* @return array
|
||||
*/
|
||||
public function get_by_type( string $type ): array {
|
||||
return array_filter( $this->get_all(), fn( $s ) => ( $s['type'] ?? '' ) === $type );
|
||||
}
|
||||
/**
|
||||
* 按类型获取源
|
||||
*
|
||||
* @param string $type 源类型
|
||||
* @return array
|
||||
*/
|
||||
public function get_by_type( string $type ): array {
|
||||
return array_filter( $this->get_all(), fn( $s ) => ( $s['type'] ?? '' ) === $type );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个源
|
||||
*
|
||||
* @param string $source_key 源唯一键
|
||||
* @return array|null
|
||||
*/
|
||||
public function get( string $source_key ): ?array {
|
||||
foreach ( $this->get_all() as $source ) {
|
||||
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
|
||||
return $source;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 获取单个源
|
||||
*
|
||||
* @param string $source_key 源唯一键
|
||||
* @return array|null
|
||||
*/
|
||||
public function get( string $source_key ): ?array {
|
||||
foreach ( $this->get_all() as $source ) {
|
||||
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
|
||||
return $source;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 DID 获取源
|
||||
*
|
||||
* @param string $did 源 DID
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_by_did( string $did ): ?array {
|
||||
foreach ( $this->get_all() as $source ) {
|
||||
if ( ( $source['did'] ?? '' ) === $did ) {
|
||||
return $source;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 通过 DID 获取源
|
||||
*
|
||||
* @param string $did 源 DID
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_by_did( string $did ): ?array {
|
||||
foreach ( $this->get_all() as $source ) {
|
||||
if ( ( $source['did'] ?? '' ) === $did ) {
|
||||
return $source;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加源
|
||||
*
|
||||
* @param array $source 源数据
|
||||
* @return string|false 成功返回 source_key,失败返回 false
|
||||
*/
|
||||
public function add( array $source ) {
|
||||
$sources = $this->get_all();
|
||||
/**
|
||||
* 添加源
|
||||
*
|
||||
* @param array $source 源数据
|
||||
* @return string|false 成功返回 source_key,失败返回 false
|
||||
*/
|
||||
public function add( array $source ) {
|
||||
$sources = $this->get_all();
|
||||
|
||||
if ( empty( $source['source_key'] ) ) {
|
||||
$source['source_key'] = 'src_' . wp_generate_uuid4();
|
||||
}
|
||||
if ( empty( $source['source_key'] ) ) {
|
||||
$source['source_key'] = 'src_' . wp_generate_uuid4();
|
||||
}
|
||||
|
||||
if ( $this->get( $source['source_key'] ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( $this->get( $source['source_key'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$source = $this->normalize_source( $source );
|
||||
$sources[] = $source;
|
||||
$this->cached_sources = $sources;
|
||||
$source = $this->normalize_source( $source );
|
||||
$sources[] = $source;
|
||||
$this->cached_sources = $sources;
|
||||
|
||||
if ( update_option( self::OPTION_NAME, $sources, false ) ) {
|
||||
return $source['source_key'];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if ( update_option( self::OPTION_NAME, $sources, false ) ) {
|
||||
return $source['source_key'];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新源
|
||||
*
|
||||
* @param string $source_key 源唯一键
|
||||
* @param array $data 更新数据
|
||||
* @return bool
|
||||
*/
|
||||
public function update( string $source_key, array $data ): bool {
|
||||
$sources = $this->get_all();
|
||||
/**
|
||||
* 更新源
|
||||
*
|
||||
* @param string $source_key 源唯一键
|
||||
* @param array $data 更新数据
|
||||
* @return bool
|
||||
*/
|
||||
public function update( string $source_key, array $data ): bool {
|
||||
$sources = $this->get_all();
|
||||
|
||||
foreach ( $sources as $index => $source ) {
|
||||
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
|
||||
unset( $data['source_key'] );
|
||||
$sources[ $index ] = array_merge( $source, $data );
|
||||
$sources[ $index ]['updated_at'] = current_time( 'mysql' );
|
||||
$this->cached_sources = $sources;
|
||||
return update_option( self::OPTION_NAME, $sources, false );
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
foreach ( $sources as $index => $source ) {
|
||||
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
|
||||
unset( $data['source_key'] );
|
||||
$sources[ $index ] = array_merge( $source, $data );
|
||||
$sources[ $index ]['updated_at'] = current_time( 'mysql' );
|
||||
$this->cached_sources = $sources;
|
||||
return update_option( self::OPTION_NAME, $sources, false );
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除源
|
||||
*
|
||||
* @param string $source_key 源唯一键
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $source_key ): bool {
|
||||
$sources = $this->get_all();
|
||||
/**
|
||||
* 删除源
|
||||
*
|
||||
* @param string $source_key 源唯一键
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $source_key ): bool {
|
||||
$sources = $this->get_all();
|
||||
|
||||
foreach ( $sources as $index => $source ) {
|
||||
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
|
||||
if ( ! empty( $source['is_preset'] ) ) {
|
||||
return false;
|
||||
}
|
||||
unset( $sources[ $index ] );
|
||||
$sources = array_values( $sources );
|
||||
$this->cached_sources = $sources;
|
||||
return update_option( self::OPTION_NAME, $sources, false );
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
foreach ( $sources as $index => $source ) {
|
||||
if ( ( $source['source_key'] ?? '' ) === $source_key ) {
|
||||
if ( ! empty( $source['is_preset'] ) ) {
|
||||
return false;
|
||||
}
|
||||
unset( $sources[ $index ] );
|
||||
$sources = array_values( $sources );
|
||||
$this->cached_sources = $sources;
|
||||
return update_option( self::OPTION_NAME, $sources, false );
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用源
|
||||
*
|
||||
* @param string $source_key 源唯一键
|
||||
* @param bool $enabled 是否启用
|
||||
* @return bool
|
||||
*/
|
||||
public function toggle( string $source_key, bool $enabled ): bool {
|
||||
return $this->update( $source_key, array( 'enabled' => $enabled ) );
|
||||
}
|
||||
/**
|
||||
* 启用/禁用源
|
||||
*
|
||||
* @param string $source_key 源唯一键
|
||||
* @param bool $enabled 是否启用
|
||||
* @return bool
|
||||
*/
|
||||
public function toggle( string $source_key, bool $enabled ): bool {
|
||||
return $this->update( $source_key, [ 'enabled' => $enabled ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化源数据
|
||||
*
|
||||
* @param array $source 源数据
|
||||
* @return array
|
||||
*/
|
||||
private function normalize_source( array $source ): array {
|
||||
return wp_parse_args(
|
||||
$source,
|
||||
array(
|
||||
'source_key' => '',
|
||||
'name' => '',
|
||||
'type' => self::TYPE_CUSTOM,
|
||||
'base_url' => '',
|
||||
'api_url' => '',
|
||||
'did' => '',
|
||||
'public_key' => '',
|
||||
'signature_scheme' => self::SIGNATURE_NONE,
|
||||
'signature_required' => false,
|
||||
'trust_level' => 50,
|
||||
'enabled' => true,
|
||||
'default_priority' => 50,
|
||||
'auth_type' => self::AUTH_NONE,
|
||||
'auth_secret_ref' => '',
|
||||
'headers' => array(),
|
||||
'capabilities' => array(),
|
||||
'rate_limit' => array(),
|
||||
'cache_ttl' => 43200,
|
||||
'is_preset' => false,
|
||||
'last_checked_at' => null,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 规范化源数据
|
||||
*
|
||||
* @param array $source 源数据
|
||||
* @return array
|
||||
*/
|
||||
private function normalize_source( array $source ): array {
|
||||
return wp_parse_args( $source, [
|
||||
'source_key' => '',
|
||||
'name' => '',
|
||||
'type' => self::TYPE_CUSTOM,
|
||||
'base_url' => '',
|
||||
'api_url' => '',
|
||||
'did' => '',
|
||||
'public_key' => '',
|
||||
'signature_scheme' => self::SIGNATURE_NONE,
|
||||
'signature_required' => false,
|
||||
'trust_level' => 50,
|
||||
'enabled' => true,
|
||||
'default_priority' => 50,
|
||||
'auth_type' => self::AUTH_NONE,
|
||||
'auth_secret_ref' => '',
|
||||
'headers' => [],
|
||||
'capabilities' => [],
|
||||
'rate_limit' => [],
|
||||
'cache_ttl' => 43200,
|
||||
'is_preset' => false,
|
||||
'last_checked_at' => null,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保预置源存在
|
||||
*/
|
||||
private function ensure_preset_sources(): void {
|
||||
$existing_keys = array_column( $this->cached_sources, 'source_key' );
|
||||
$needs_update = false;
|
||||
/**
|
||||
* 确保预置源存在
|
||||
*/
|
||||
private function ensure_preset_sources(): void {
|
||||
$existing_keys = array_column( $this->cached_sources, 'source_key' );
|
||||
$needs_update = false;
|
||||
|
||||
foreach ( $this->get_preset_sources() as $preset ) {
|
||||
if ( ! in_array( $preset['source_key'], $existing_keys, true ) ) {
|
||||
$this->cached_sources[] = $preset;
|
||||
$needs_update = true;
|
||||
}
|
||||
}
|
||||
foreach ( $this->get_preset_sources() as $preset ) {
|
||||
if ( ! in_array( $preset['source_key'], $existing_keys, true ) ) {
|
||||
$this->cached_sources[] = $preset;
|
||||
$needs_update = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $needs_update ) {
|
||||
update_option( self::OPTION_NAME, $this->cached_sources, false );
|
||||
}
|
||||
}
|
||||
if ( $needs_update ) {
|
||||
update_option( self::OPTION_NAME, $this->cached_sources, false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预置源列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_preset_sources(): array {
|
||||
$now = current_time( 'mysql' );
|
||||
return array(
|
||||
array(
|
||||
'source_key' => 'wporg',
|
||||
'name' => 'WordPress.org',
|
||||
'type' => self::TYPE_WPORG,
|
||||
'base_url' => 'https://wordpress.org',
|
||||
'api_url' => 'https://api.wordpress.org',
|
||||
'did' => '',
|
||||
'public_key' => '',
|
||||
'signature_scheme' => self::SIGNATURE_NONE,
|
||||
'signature_required' => false,
|
||||
'trust_level' => 100,
|
||||
'enabled' => true,
|
||||
'default_priority' => 100,
|
||||
'auth_type' => self::AUTH_NONE,
|
||||
'auth_secret_ref' => '',
|
||||
'headers' => array(),
|
||||
'capabilities' => array( 'plugins', 'themes', 'core', 'translations' ),
|
||||
'rate_limit' => array(),
|
||||
'cache_ttl' => 43200,
|
||||
'is_preset' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
),
|
||||
array(
|
||||
'source_key' => 'wenpai-mirror',
|
||||
'name' => '文派开源更新源',
|
||||
'type' => self::TYPE_JSON,
|
||||
'base_url' => 'https://wenpai.org',
|
||||
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
|
||||
'did' => '',
|
||||
'public_key' => '',
|
||||
'signature_scheme' => self::SIGNATURE_NONE,
|
||||
'signature_required' => false,
|
||||
'trust_level' => 90,
|
||||
'enabled' => true,
|
||||
'default_priority' => 10,
|
||||
'auth_type' => self::AUTH_NONE,
|
||||
'auth_secret_ref' => '',
|
||||
'headers' => array(),
|
||||
'capabilities' => array( 'plugins', 'themes' ),
|
||||
'rate_limit' => array(),
|
||||
'cache_ttl' => 43200,
|
||||
'is_preset' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
),
|
||||
array(
|
||||
'source_key' => 'fair-aspirecloud',
|
||||
'name' => 'FAIR AspireCloud',
|
||||
'type' => self::TYPE_FAIR,
|
||||
'base_url' => 'https://aspirepress.org',
|
||||
'api_url' => 'https://api.aspirecloud.io/v1',
|
||||
'did' => '',
|
||||
'public_key' => '',
|
||||
'signature_scheme' => self::SIGNATURE_ED25519,
|
||||
'signature_required' => false,
|
||||
'trust_level' => 85,
|
||||
'enabled' => false,
|
||||
'default_priority' => 20,
|
||||
'auth_type' => self::AUTH_NONE,
|
||||
'auth_secret_ref' => '',
|
||||
'headers' => array(),
|
||||
'capabilities' => array( 'plugins', 'themes', 'fair_did' ),
|
||||
'rate_limit' => array(),
|
||||
'cache_ttl' => 43200,
|
||||
'is_preset' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取预置源列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_preset_sources(): array {
|
||||
$now = current_time( 'mysql' );
|
||||
return [
|
||||
[
|
||||
'source_key' => 'wporg',
|
||||
'name' => 'WordPress.org',
|
||||
'type' => self::TYPE_WPORG,
|
||||
'base_url' => 'https://wordpress.org',
|
||||
'api_url' => 'https://api.wordpress.org',
|
||||
'did' => '',
|
||||
'public_key' => '',
|
||||
'signature_scheme' => self::SIGNATURE_NONE,
|
||||
'signature_required' => false,
|
||||
'trust_level' => 100,
|
||||
'enabled' => true,
|
||||
'default_priority' => 100,
|
||||
'auth_type' => self::AUTH_NONE,
|
||||
'auth_secret_ref' => '',
|
||||
'headers' => [],
|
||||
'capabilities' => [ 'plugins', 'themes', 'core', 'translations' ],
|
||||
'rate_limit' => [],
|
||||
'cache_ttl' => 43200,
|
||||
'is_preset' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'source_key' => 'wenpai-mirror',
|
||||
'name' => '文派开源更新源',
|
||||
'type' => self::TYPE_JSON,
|
||||
'base_url' => 'https://wenpai.org',
|
||||
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
|
||||
'did' => '',
|
||||
'public_key' => '',
|
||||
'signature_scheme' => self::SIGNATURE_NONE,
|
||||
'signature_required' => false,
|
||||
'trust_level' => 90,
|
||||
'enabled' => true,
|
||||
'default_priority' => 10,
|
||||
'auth_type' => self::AUTH_NONE,
|
||||
'auth_secret_ref' => '',
|
||||
'headers' => [],
|
||||
'capabilities' => [ 'plugins', 'themes' ],
|
||||
'rate_limit' => [],
|
||||
'cache_ttl' => 43200,
|
||||
'is_preset' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'source_key' => 'fair-aspirecloud',
|
||||
'name' => 'FAIR AspireCloud',
|
||||
'type' => self::TYPE_FAIR,
|
||||
'base_url' => 'https://aspirepress.org',
|
||||
'api_url' => 'https://api.aspirecloud.io/v1',
|
||||
'did' => '',
|
||||
'public_key' => '',
|
||||
'signature_scheme' => self::SIGNATURE_ED25519,
|
||||
'signature_required' => false,
|
||||
'trust_level' => 85,
|
||||
'enabled' => false,
|
||||
'default_priority' => 20,
|
||||
'auth_type' => self::AUTH_NONE,
|
||||
'auth_secret_ref' => '',
|
||||
'headers' => [],
|
||||
'capabilities' => [ 'plugins', 'themes', 'fair_did' ],
|
||||
'rate_limit' => [],
|
||||
'cache_ttl' => 43200,
|
||||
'is_preset' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取源类型标签
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_type_labels(): array {
|
||||
return array(
|
||||
self::TYPE_WPORG => 'WordPress.org',
|
||||
self::TYPE_FAIR => 'FAIR',
|
||||
self::TYPE_CUSTOM => '自定义',
|
||||
self::TYPE_GIT => 'Git 仓库',
|
||||
self::TYPE_MIRROR => '镜像',
|
||||
self::TYPE_JSON => 'JSON API',
|
||||
self::TYPE_ARKPRESS => 'ArkPress',
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取源类型标签
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_type_labels(): array {
|
||||
return [
|
||||
self::TYPE_WPORG => 'WordPress.org',
|
||||
self::TYPE_FAIR => 'FAIR',
|
||||
self::TYPE_CUSTOM => '自定义',
|
||||
self::TYPE_GIT => 'Git 仓库',
|
||||
self::TYPE_MIRROR => '镜像',
|
||||
self::TYPE_JSON => 'JSON API',
|
||||
self::TYPE_ARKPRESS => 'ArkPress',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->cached_sources = null;
|
||||
}
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->cached_sources = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\Core;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,289 +17,285 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class VersionLock {
|
||||
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_version_locks';
|
||||
/**
|
||||
* 选项名称
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_version_locks';
|
||||
|
||||
/**
|
||||
* 锁定类型常量
|
||||
*/
|
||||
const LOCK_CURRENT = 'current'; // 锁定到当前版本
|
||||
const LOCK_SPECIFIC = 'specific'; // 锁定到指定版本
|
||||
const LOCK_IGNORE = 'ignore'; // 忽略特定版本
|
||||
/**
|
||||
* 锁定类型常量
|
||||
*/
|
||||
const LOCK_CURRENT = 'current'; // 锁定到当前版本
|
||||
const LOCK_SPECIFIC = 'specific'; // 锁定到指定版本
|
||||
const LOCK_IGNORE = 'ignore'; // 忽略特定版本
|
||||
|
||||
/**
|
||||
* 单例实例
|
||||
*
|
||||
* @var VersionLock|null
|
||||
*/
|
||||
private static ?VersionLock $instance = null;
|
||||
/**
|
||||
* 单例实例
|
||||
*
|
||||
* @var VersionLock|null
|
||||
*/
|
||||
private static ?VersionLock $instance = null;
|
||||
|
||||
/**
|
||||
* 锁定数据缓存
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $locks = null;
|
||||
/**
|
||||
* 锁定数据缓存
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private ?array $locks = null;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*
|
||||
* @return VersionLock
|
||||
*/
|
||||
public static function get_instance(): VersionLock {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
/**
|
||||
* 获取单例实例
|
||||
*
|
||||
* @return VersionLock
|
||||
*/
|
||||
public static function get_instance(): VersionLock {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有构造函数
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
}
|
||||
/**
|
||||
* 私有构造函数
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 过滤插件更新
|
||||
add_filter( 'site_transient_update_plugins', array( $this, 'filter_plugin_updates' ), 100 );
|
||||
// 过滤主题更新
|
||||
add_filter( 'site_transient_update_themes', array( $this, 'filter_theme_updates' ), 100 );
|
||||
}
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 过滤插件更新
|
||||
add_filter( 'site_transient_update_plugins', [ $this, 'filter_plugin_updates' ], 100 );
|
||||
// 过滤主题更新
|
||||
add_filter( 'site_transient_update_themes', [ $this, 'filter_theme_updates' ], 100 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有锁定
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->locks ) {
|
||||
$this->locks = get_option( self::OPTION_NAME, array() );
|
||||
if ( ! is_array( $this->locks ) ) {
|
||||
$this->locks = array();
|
||||
}
|
||||
}
|
||||
return $this->locks;
|
||||
}
|
||||
/**
|
||||
* 获取所有锁定
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all(): array {
|
||||
if ( null === $this->locks ) {
|
||||
$this->locks = get_option( self::OPTION_NAME, [] );
|
||||
if ( ! is_array( $this->locks ) ) {
|
||||
$this->locks = [];
|
||||
}
|
||||
}
|
||||
return $this->locks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目的锁定信息
|
||||
*
|
||||
* @param string $item_key 项目键(如 plugin:hello.php 或 theme:flavor)
|
||||
* @return array|null
|
||||
*/
|
||||
public function get( string $item_key ): ?array {
|
||||
$locks = $this->get_all();
|
||||
return $locks[ $item_key ] ?? null;
|
||||
}
|
||||
/**
|
||||
* 获取项目的锁定信息
|
||||
*
|
||||
* @param string $item_key 项目键(如 plugin:hello.php 或 theme:flavor)
|
||||
* @return array|null
|
||||
*/
|
||||
public function get( string $item_key ): ?array {
|
||||
$locks = $this->get_all();
|
||||
return $locks[ $item_key ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定项目版本
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $lock_type 锁定类型
|
||||
* @param string $version 版本号(锁定到指定版本时使用)
|
||||
* @param array $ignore_versions 忽略的版本列表
|
||||
* @return bool
|
||||
*/
|
||||
public function lock( string $item_key, string $lock_type, string $version = '', array $ignore_versions = array() ): bool {
|
||||
$locks = $this->get_all();
|
||||
/**
|
||||
* 锁定项目版本
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $lock_type 锁定类型
|
||||
* @param string $version 版本号(锁定到指定版本时使用)
|
||||
* @param array $ignore_versions 忽略的版本列表
|
||||
* @return bool
|
||||
*/
|
||||
public function lock( string $item_key, string $lock_type, string $version = '', array $ignore_versions = [] ): bool {
|
||||
$locks = $this->get_all();
|
||||
|
||||
$locks[ $item_key ] = array(
|
||||
'type' => $lock_type,
|
||||
'version' => $version,
|
||||
'ignore_versions' => $ignore_versions,
|
||||
'locked_at' => current_time( 'mysql' ),
|
||||
);
|
||||
$locks[ $item_key ] = [
|
||||
'type' => $lock_type,
|
||||
'version' => $version,
|
||||
'ignore_versions' => $ignore_versions,
|
||||
'locked_at' => current_time( 'mysql' ),
|
||||
];
|
||||
|
||||
$this->locks = $locks;
|
||||
return update_option( self::OPTION_NAME, $locks );
|
||||
}
|
||||
$this->locks = $locks;
|
||||
return update_option( self::OPTION_NAME, $locks );
|
||||
}
|
||||
|
||||
/**
|
||||
* 解锁项目
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return bool
|
||||
*/
|
||||
public function unlock( string $item_key ): bool {
|
||||
$locks = $this->get_all();
|
||||
/**
|
||||
* 解锁项目
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return bool
|
||||
*/
|
||||
public function unlock( string $item_key ): bool {
|
||||
$locks = $this->get_all();
|
||||
|
||||
if ( ! isset( $locks[ $item_key ] ) ) {
|
||||
return true;
|
||||
}
|
||||
if ( ! isset( $locks[ $item_key ] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
unset( $locks[ $item_key ] );
|
||||
$this->locks = $locks;
|
||||
return update_option( self::OPTION_NAME, $locks );
|
||||
}
|
||||
unset( $locks[ $item_key ] );
|
||||
$this->locks = $locks;
|
||||
return update_option( self::OPTION_NAME, $locks );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查项目是否被锁定
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return bool
|
||||
*/
|
||||
public function is_locked( string $item_key ): bool {
|
||||
return null !== $this->get( $item_key );
|
||||
}
|
||||
/**
|
||||
* 检查项目是否被锁定
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @return bool
|
||||
*/
|
||||
public function is_locked( string $item_key ): bool {
|
||||
return null !== $this->get( $item_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该阻止更新
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $current_version 当前版本
|
||||
* @param string $new_version 新版本
|
||||
* @return bool 返回 true 表示应该阻止更新
|
||||
*/
|
||||
public function should_block_update( string $item_key, string $current_version, string $new_version ): bool {
|
||||
$lock = $this->get( $item_key );
|
||||
/**
|
||||
* 检查是否应该阻止更新
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $current_version 当前版本
|
||||
* @param string $new_version 新版本
|
||||
* @return bool 返回 true 表示应该阻止更新
|
||||
*/
|
||||
public function should_block_update( string $item_key, string $current_version, string $new_version ): bool {
|
||||
$lock = $this->get( $item_key );
|
||||
|
||||
if ( null === $lock ) {
|
||||
return false;
|
||||
}
|
||||
if ( null === $lock ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ( $lock['type'] ) {
|
||||
case self::LOCK_CURRENT:
|
||||
// 锁定到当前版本,阻止所有更新
|
||||
return true;
|
||||
switch ( $lock['type'] ) {
|
||||
case self::LOCK_CURRENT:
|
||||
// 锁定到当前版本,阻止所有更新
|
||||
return true;
|
||||
|
||||
case self::LOCK_SPECIFIC:
|
||||
// 锁定到指定版本,如果当前版本等于锁定版本则阻止更新
|
||||
return version_compare( $current_version, $lock['version'], '==' );
|
||||
case self::LOCK_SPECIFIC:
|
||||
// 锁定到指定版本,如果当前版本等于锁定版本则阻止更新
|
||||
return version_compare( $current_version, $lock['version'], '==' );
|
||||
|
||||
case self::LOCK_IGNORE:
|
||||
// 忽略特定版本
|
||||
return in_array( $new_version, $lock['ignore_versions'], true );
|
||||
case self::LOCK_IGNORE:
|
||||
// 忽略特定版本
|
||||
return in_array( $new_version, $lock['ignore_versions'], true );
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤插件更新
|
||||
*
|
||||
* @param object $transient 更新 transient
|
||||
* @return object
|
||||
*/
|
||||
public function filter_plugin_updates( $transient ) {
|
||||
if ( empty( $transient ) || ! is_object( $transient ) ) {
|
||||
return $transient;
|
||||
}
|
||||
/**
|
||||
* 过滤插件更新
|
||||
*
|
||||
* @param object $transient 更新 transient
|
||||
* @return object
|
||||
*/
|
||||
public function filter_plugin_updates( $transient ) {
|
||||
if ( empty( $transient ) || ! is_object( $transient ) ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
if ( empty( $transient->response ) ) {
|
||||
return $transient;
|
||||
}
|
||||
if ( empty( $transient->response ) ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
$plugins = get_plugins();
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
$plugins = get_plugins();
|
||||
|
||||
foreach ( $transient->response as $plugin_file => $update_info ) {
|
||||
$item_key = 'plugin:' . $plugin_file;
|
||||
$current_version = $plugins[ $plugin_file ]['Version'] ?? '0';
|
||||
$new_version = is_object( $update_info ) ? $update_info->new_version : ( $update_info['new_version'] ?? '0' );
|
||||
foreach ( $transient->response as $plugin_file => $update_info ) {
|
||||
$item_key = 'plugin:' . $plugin_file;
|
||||
$current_version = $plugins[ $plugin_file ]['Version'] ?? '0';
|
||||
$new_version = is_object( $update_info ) ? $update_info->new_version : ( $update_info['new_version'] ?? '0' );
|
||||
|
||||
if ( $this->should_block_update( $item_key, $current_version, $new_version ) ) {
|
||||
// 移动到 no_update
|
||||
if ( ! isset( $transient->no_update ) ) {
|
||||
$transient->no_update = array();
|
||||
}
|
||||
$transient->no_update[ $plugin_file ] = $update_info;
|
||||
unset( $transient->response[ $plugin_file ] );
|
||||
if ( $this->should_block_update( $item_key, $current_version, $new_version ) ) {
|
||||
// 移动到 no_update
|
||||
if ( ! isset( $transient->no_update ) ) {
|
||||
$transient->no_update = [];
|
||||
}
|
||||
$transient->no_update[ $plugin_file ] = $update_info;
|
||||
unset( $transient->response[ $plugin_file ] );
|
||||
|
||||
Logger::debug(
|
||||
sprintf(
|
||||
'Version lock: blocked update for %s (current: %s, new: %s)',
|
||||
$plugin_file,
|
||||
$current_version,
|
||||
$new_version
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Logger::debug( sprintf(
|
||||
'Version lock: blocked update for %s (current: %s, new: %s)',
|
||||
$plugin_file,
|
||||
$current_version,
|
||||
$new_version
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
return $transient;
|
||||
}
|
||||
return $transient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤主题更新
|
||||
*
|
||||
* @param object $transient 更新 transient
|
||||
* @return object
|
||||
*/
|
||||
public function filter_theme_updates( $transient ) {
|
||||
if ( empty( $transient ) || ! is_object( $transient ) ) {
|
||||
return $transient;
|
||||
}
|
||||
/**
|
||||
* 过滤主题更新
|
||||
*
|
||||
* @param object $transient 更新 transient
|
||||
* @return object
|
||||
*/
|
||||
public function filter_theme_updates( $transient ) {
|
||||
if ( empty( $transient ) || ! is_object( $transient ) ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
if ( empty( $transient->response ) ) {
|
||||
return $transient;
|
||||
}
|
||||
if ( empty( $transient->response ) ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
$themes = wp_get_themes();
|
||||
$themes = wp_get_themes();
|
||||
|
||||
foreach ( $transient->response as $theme_slug => $update_info ) {
|
||||
$item_key = 'theme:' . $theme_slug;
|
||||
$current_version = isset( $themes[ $theme_slug ] ) ? $themes[ $theme_slug ]->get( 'Version' ) : '0';
|
||||
$new_version = is_array( $update_info ) ? ( $update_info['new_version'] ?? '0' ) : '0';
|
||||
foreach ( $transient->response as $theme_slug => $update_info ) {
|
||||
$item_key = 'theme:' . $theme_slug;
|
||||
$current_version = isset( $themes[ $theme_slug ] ) ? $themes[ $theme_slug ]->get( 'Version' ) : '0';
|
||||
$new_version = is_array( $update_info ) ? ( $update_info['new_version'] ?? '0' ) : '0';
|
||||
|
||||
if ( $this->should_block_update( $item_key, $current_version, $new_version ) ) {
|
||||
unset( $transient->response[ $theme_slug ] );
|
||||
if ( $this->should_block_update( $item_key, $current_version, $new_version ) ) {
|
||||
unset( $transient->response[ $theme_slug ] );
|
||||
|
||||
Logger::debug(
|
||||
sprintf(
|
||||
'Version lock: blocked update for theme %s (current: %s, new: %s)',
|
||||
$theme_slug,
|
||||
$current_version,
|
||||
$new_version
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Logger::debug( sprintf(
|
||||
'Version lock: blocked update for theme %s (current: %s, new: %s)',
|
||||
$theme_slug,
|
||||
$current_version,
|
||||
$new_version
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
return $transient;
|
||||
}
|
||||
return $transient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁定类型标签
|
||||
*
|
||||
* @param string $type 锁定类型
|
||||
* @return string
|
||||
*/
|
||||
public static function get_type_label( string $type ): string {
|
||||
$labels = array(
|
||||
self::LOCK_CURRENT => __( '锁定当前版本', 'wpbridge' ),
|
||||
self::LOCK_SPECIFIC => __( '锁定指定版本', 'wpbridge' ),
|
||||
self::LOCK_IGNORE => __( '忽略特定版本', 'wpbridge' ),
|
||||
);
|
||||
return $labels[ $type ] ?? $type;
|
||||
}
|
||||
/**
|
||||
* 获取锁定类型标签
|
||||
*
|
||||
* @param string $type 锁定类型
|
||||
* @return string
|
||||
*/
|
||||
public static function get_type_label( string $type ): string {
|
||||
$labels = [
|
||||
self::LOCK_CURRENT => __( '锁定当前版本', 'wpbridge' ),
|
||||
self::LOCK_SPECIFIC => __( '锁定指定版本', 'wpbridge' ),
|
||||
self::LOCK_IGNORE => __( '忽略特定版本', 'wpbridge' ),
|
||||
];
|
||||
return $labels[ $type ] ?? $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有锁定类型
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_lock_types(): array {
|
||||
return array(
|
||||
self::LOCK_CURRENT => __( '锁定当前版本', 'wpbridge' ),
|
||||
self::LOCK_SPECIFIC => __( '锁定指定版本', 'wpbridge' ),
|
||||
self::LOCK_IGNORE => __( '忽略特定版本', 'wpbridge' ),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取所有锁定类型
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_lock_types(): array {
|
||||
return [
|
||||
self::LOCK_CURRENT => __( '锁定当前版本', 'wpbridge' ),
|
||||
self::LOCK_SPECIFIC => __( '锁定指定版本', 'wpbridge' ),
|
||||
self::LOCK_IGNORE => __( '忽略特定版本', 'wpbridge' ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->locks = null;
|
||||
}
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->locks = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ namespace WPBridge\FAIR;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,301 +22,301 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class FairProtocol {
|
||||
|
||||
/**
|
||||
* DID 方法前缀
|
||||
*/
|
||||
const DID_METHOD = 'did:fair:';
|
||||
/**
|
||||
* DID 方法前缀
|
||||
*/
|
||||
const DID_METHOD = 'did:fair:';
|
||||
|
||||
/**
|
||||
* 支持的签名方案
|
||||
*/
|
||||
const SIGNATURE_ED25519 = 'ed25519';
|
||||
/**
|
||||
* 支持的签名方案
|
||||
*/
|
||||
const SIGNATURE_ED25519 = 'ed25519';
|
||||
|
||||
/**
|
||||
* 解析 FAIR DID
|
||||
*
|
||||
* DID 格式: did:fair:<namespace>:<identifier>
|
||||
* 例如: did:fair:wp:plugin:hello-dolly
|
||||
*
|
||||
* @param string $did DID 字符串
|
||||
* @return array|null 解析结果
|
||||
*/
|
||||
public function parse_did( string $did ): ?array {
|
||||
if ( strpos( $did, self::DID_METHOD ) !== 0 ) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 解析 FAIR DID
|
||||
*
|
||||
* DID 格式: did:fair:<namespace>:<identifier>
|
||||
* 例如: did:fair:wp:plugin:hello-dolly
|
||||
*
|
||||
* @param string $did DID 字符串
|
||||
* @return array|null 解析结果
|
||||
*/
|
||||
public function parse_did( string $did ): ?array {
|
||||
if ( strpos( $did, self::DID_METHOD ) !== 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = explode( ':', substr( $did, strlen( self::DID_METHOD ) ) );
|
||||
$parts = explode( ':', substr( $did, strlen( self::DID_METHOD ) ) );
|
||||
|
||||
if ( count( $parts ) < 2 ) {
|
||||
return null;
|
||||
}
|
||||
if ( count( $parts ) < 2 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'method' => 'fair',
|
||||
'namespace' => $parts[0] ?? '',
|
||||
'type' => $parts[1] ?? '',
|
||||
'identifier' => $parts[2] ?? '',
|
||||
'version' => $parts[3] ?? null,
|
||||
'raw' => $did,
|
||||
);
|
||||
}
|
||||
return [
|
||||
'method' => 'fair',
|
||||
'namespace' => $parts[0] ?? '',
|
||||
'type' => $parts[1] ?? '',
|
||||
'identifier' => $parts[2] ?? '',
|
||||
'version' => $parts[3] ?? null,
|
||||
'raw' => $did,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 FAIR DID
|
||||
*
|
||||
* @param string $namespace 命名空间 (如 'wp')
|
||||
* @param string $type 类型 (如 'plugin', 'theme')
|
||||
* @param string $identifier 标识符 (如 'hello-dolly')
|
||||
* @param string|null $version 版本号 (可选)
|
||||
* @return string
|
||||
*/
|
||||
public function build_did( string $namespace, string $type, string $identifier, ?string $version = null ): string {
|
||||
$did = self::DID_METHOD . $namespace . ':' . $type . ':' . $identifier;
|
||||
/**
|
||||
* 构建 FAIR DID
|
||||
*
|
||||
* @param string $namespace 命名空间 (如 'wp')
|
||||
* @param string $type 类型 (如 'plugin', 'theme')
|
||||
* @param string $identifier 标识符 (如 'hello-dolly')
|
||||
* @param string|null $version 版本号 (可选)
|
||||
* @return string
|
||||
*/
|
||||
public function build_did( string $namespace, string $type, string $identifier, ?string $version = null ): string {
|
||||
$did = self::DID_METHOD . $namespace . ':' . $type . ':' . $identifier;
|
||||
|
||||
if ( $version ) {
|
||||
$did .= ':' . $version;
|
||||
}
|
||||
if ( $version ) {
|
||||
$did .= ':' . $version;
|
||||
}
|
||||
|
||||
return $did;
|
||||
}
|
||||
return $did;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 WordPress 项目生成 DID
|
||||
*
|
||||
* @param string $item_key 项目键 (如 'plugin:hello-dolly/hello.php')
|
||||
* @param string $item_slug 项目 slug
|
||||
* @return string
|
||||
*/
|
||||
public function generate_did_from_item( string $item_key, string $item_slug ): string {
|
||||
$type = 'plugin';
|
||||
/**
|
||||
* 从 WordPress 项目生成 DID
|
||||
*
|
||||
* @param string $item_key 项目键 (如 'plugin:hello-dolly/hello.php')
|
||||
* @param string $item_slug 项目 slug
|
||||
* @return string
|
||||
*/
|
||||
public function generate_did_from_item( string $item_key, string $item_slug ): string {
|
||||
$type = 'plugin';
|
||||
|
||||
if ( strpos( $item_key, 'theme:' ) === 0 ) {
|
||||
$type = 'theme';
|
||||
} elseif ( strpos( $item_key, 'mu-plugin:' ) === 0 ) {
|
||||
$type = 'mu-plugin';
|
||||
}
|
||||
if ( strpos( $item_key, 'theme:' ) === 0 ) {
|
||||
$type = 'theme';
|
||||
} elseif ( strpos( $item_key, 'mu-plugin:' ) === 0 ) {
|
||||
$type = 'mu-plugin';
|
||||
}
|
||||
|
||||
return $this->build_did( 'wp', $type, $item_slug );
|
||||
}
|
||||
return $this->build_did( 'wp', $type, $item_slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 ED25519 签名
|
||||
*
|
||||
* @param string $message 原始消息
|
||||
* @param string $signature 签名 (base64 编码)
|
||||
* @param string $public_key 公钥 (base64 编码)
|
||||
* @return bool
|
||||
*/
|
||||
public function verify_ed25519_signature( string $message, string $signature, string $public_key ): bool {
|
||||
// 检查 sodium 扩展
|
||||
if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ) {
|
||||
return $this->verify_ed25519_fallback( $message, $signature, $public_key );
|
||||
}
|
||||
/**
|
||||
* 验证 ED25519 签名
|
||||
*
|
||||
* @param string $message 原始消息
|
||||
* @param string $signature 签名 (base64 编码)
|
||||
* @param string $public_key 公钥 (base64 编码)
|
||||
* @return bool
|
||||
*/
|
||||
public function verify_ed25519_signature( string $message, string $signature, string $public_key ): bool {
|
||||
// 检查 sodium 扩展
|
||||
if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ) {
|
||||
return $this->verify_ed25519_fallback( $message, $signature, $public_key );
|
||||
}
|
||||
|
||||
try {
|
||||
$signature_bin = base64_decode( $signature, true );
|
||||
$public_key_bin = base64_decode( $public_key, true );
|
||||
try {
|
||||
$signature_bin = base64_decode( $signature, true );
|
||||
$public_key_bin = base64_decode( $public_key, true );
|
||||
|
||||
if ( false === $signature_bin || false === $public_key_bin ) {
|
||||
return false;
|
||||
}
|
||||
if ( false === $signature_bin || false === $public_key_bin ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证签名长度
|
||||
if ( strlen( $signature_bin ) !== SODIUM_CRYPTO_SIGN_BYTES ) {
|
||||
return false;
|
||||
}
|
||||
// 验证签名长度
|
||||
if ( strlen( $signature_bin ) !== SODIUM_CRYPTO_SIGN_BYTES ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证公钥长度
|
||||
if ( strlen( $public_key_bin ) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES ) {
|
||||
return false;
|
||||
}
|
||||
// 验证公钥长度
|
||||
if ( strlen( $public_key_bin ) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return sodium_crypto_sign_verify_detached( $signature_bin, $message, $public_key_bin );
|
||||
return sodium_crypto_sign_verify_detached( $signature_bin, $message, $public_key_bin );
|
||||
|
||||
} catch ( \Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch ( \Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ED25519 签名验证回退方案
|
||||
*
|
||||
* 当 sodium 扩展不可用时使用
|
||||
*
|
||||
* @param string $message 原始消息
|
||||
* @param string $signature 签名
|
||||
* @param string $public_key 公钥
|
||||
* @return bool
|
||||
*/
|
||||
private function verify_ed25519_fallback( string $message, string $signature, string $public_key ): bool {
|
||||
// 尝试使用 paragonie/sodium_compat
|
||||
if ( class_exists( '\ParagonIE_Sodium_Compat' ) ) {
|
||||
try {
|
||||
$signature_bin = base64_decode( $signature, true );
|
||||
$public_key_bin = base64_decode( $public_key, true );
|
||||
/**
|
||||
* ED25519 签名验证回退方案
|
||||
*
|
||||
* 当 sodium 扩展不可用时使用
|
||||
*
|
||||
* @param string $message 原始消息
|
||||
* @param string $signature 签名
|
||||
* @param string $public_key 公钥
|
||||
* @return bool
|
||||
*/
|
||||
private function verify_ed25519_fallback( string $message, string $signature, string $public_key ): bool {
|
||||
// 尝试使用 paragonie/sodium_compat
|
||||
if ( class_exists( '\ParagonIE_Sodium_Compat' ) ) {
|
||||
try {
|
||||
$signature_bin = base64_decode( $signature, true );
|
||||
$public_key_bin = base64_decode( $public_key, true );
|
||||
|
||||
if ( false === $signature_bin || false === $public_key_bin ) {
|
||||
return false;
|
||||
}
|
||||
if ( false === $signature_bin || false === $public_key_bin ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \ParagonIE_Sodium_Compat::crypto_sign_verify_detached(
|
||||
$signature_bin,
|
||||
$message,
|
||||
$public_key_bin
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return \ParagonIE_Sodium_Compat::crypto_sign_verify_detached(
|
||||
$signature_bin,
|
||||
$message,
|
||||
$public_key_bin
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 无法验证签名
|
||||
return false;
|
||||
}
|
||||
// 无法验证签名
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证包签名
|
||||
*
|
||||
* @param array $package 包数据
|
||||
* @return array 验证结果
|
||||
*/
|
||||
public function verify_package_signature( array $package ): array {
|
||||
$result = array(
|
||||
'valid' => false,
|
||||
'signed' => false,
|
||||
'signer' => null,
|
||||
'algorithm' => null,
|
||||
'error' => null,
|
||||
);
|
||||
/**
|
||||
* 验证包签名
|
||||
*
|
||||
* @param array $package 包数据
|
||||
* @return array 验证结果
|
||||
*/
|
||||
public function verify_package_signature( array $package ): array {
|
||||
$result = [
|
||||
'valid' => false,
|
||||
'signed' => false,
|
||||
'signer' => null,
|
||||
'algorithm' => null,
|
||||
'error' => null,
|
||||
];
|
||||
|
||||
// 检查是否有签名
|
||||
if ( empty( $package['signature'] ) ) {
|
||||
$result['error'] = 'no_signature';
|
||||
return $result;
|
||||
}
|
||||
// 检查是否有签名
|
||||
if ( empty( $package['signature'] ) ) {
|
||||
$result['error'] = 'no_signature';
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['signed'] = true;
|
||||
$result['signed'] = true;
|
||||
|
||||
// 获取签名信息
|
||||
$signature_data = $package['signature'];
|
||||
$algorithm = $signature_data['algorithm'] ?? self::SIGNATURE_ED25519;
|
||||
$signature = $signature_data['value'] ?? '';
|
||||
$public_key = $signature_data['public_key'] ?? '';
|
||||
$signer_did = $signature_data['signer'] ?? '';
|
||||
// 获取签名信息
|
||||
$signature_data = $package['signature'];
|
||||
$algorithm = $signature_data['algorithm'] ?? self::SIGNATURE_ED25519;
|
||||
$signature = $signature_data['value'] ?? '';
|
||||
$public_key = $signature_data['public_key'] ?? '';
|
||||
$signer_did = $signature_data['signer'] ?? '';
|
||||
|
||||
$result['algorithm'] = $algorithm;
|
||||
$result['signer'] = $signer_did;
|
||||
$result['algorithm'] = $algorithm;
|
||||
$result['signer'] = $signer_did;
|
||||
|
||||
// 目前只支持 ED25519
|
||||
if ( $algorithm !== self::SIGNATURE_ED25519 ) {
|
||||
$result['error'] = 'unsupported_algorithm';
|
||||
return $result;
|
||||
}
|
||||
// 目前只支持 ED25519
|
||||
if ( $algorithm !== self::SIGNATURE_ED25519 ) {
|
||||
$result['error'] = 'unsupported_algorithm';
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 构建待验证消息
|
||||
$message = $this->build_signature_message( $package );
|
||||
// 构建待验证消息
|
||||
$message = $this->build_signature_message( $package );
|
||||
|
||||
// 验证签名
|
||||
if ( $this->verify_ed25519_signature( $message, $signature, $public_key ) ) {
|
||||
$result['valid'] = true;
|
||||
} else {
|
||||
$result['error'] = 'invalid_signature';
|
||||
}
|
||||
// 验证签名
|
||||
if ( $this->verify_ed25519_signature( $message, $signature, $public_key ) ) {
|
||||
$result['valid'] = true;
|
||||
} else {
|
||||
$result['error'] = 'invalid_signature';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建签名消息
|
||||
*
|
||||
* @param array $package 包数据
|
||||
* @return string
|
||||
*/
|
||||
private function build_signature_message( array $package ): string {
|
||||
// 移除签名字段
|
||||
$data = $package;
|
||||
unset( $data['signature'] );
|
||||
/**
|
||||
* 构建签名消息
|
||||
*
|
||||
* @param array $package 包数据
|
||||
* @return string
|
||||
*/
|
||||
private function build_signature_message( array $package ): string {
|
||||
// 移除签名字段
|
||||
$data = $package;
|
||||
unset( $data['signature'] );
|
||||
|
||||
// 按键排序
|
||||
ksort( $data );
|
||||
// 按键排序
|
||||
ksort( $data );
|
||||
|
||||
// JSON 编码
|
||||
return wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
|
||||
}
|
||||
// JSON 编码
|
||||
return wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 FAIR 仓库响应
|
||||
*
|
||||
* @param array $response API 响应
|
||||
* @return array 解析后的包列表
|
||||
*/
|
||||
public function parse_repository_response( array $response ): array {
|
||||
$packages = array();
|
||||
/**
|
||||
* 解析 FAIR 仓库响应
|
||||
*
|
||||
* @param array $response API 响应
|
||||
* @return array 解析后的包列表
|
||||
*/
|
||||
public function parse_repository_response( array $response ): array {
|
||||
$packages = [];
|
||||
|
||||
// FAIR 响应格式
|
||||
if ( isset( $response['packages'] ) ) {
|
||||
foreach ( $response['packages'] as $package ) {
|
||||
$parsed = $this->parse_package( $package );
|
||||
if ( $parsed ) {
|
||||
$packages[] = $parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
// FAIR 响应格式
|
||||
if ( isset( $response['packages'] ) ) {
|
||||
foreach ( $response['packages'] as $package ) {
|
||||
$parsed = $this->parse_package( $package );
|
||||
if ( $parsed ) {
|
||||
$packages[] = $parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packages;
|
||||
}
|
||||
return $packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个包
|
||||
*
|
||||
* @param array $package 包数据
|
||||
* @return array|null
|
||||
*/
|
||||
private function parse_package( array $package ): ?array {
|
||||
if ( empty( $package['did'] ) && empty( $package['slug'] ) ) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 解析单个包
|
||||
*
|
||||
* @param array $package 包数据
|
||||
* @return array|null
|
||||
*/
|
||||
private function parse_package( array $package ): ?array {
|
||||
if ( empty( $package['did'] ) && empty( $package['slug'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'did' => $package['did'] ?? '',
|
||||
'slug' => $package['slug'] ?? '',
|
||||
'name' => $package['name'] ?? '',
|
||||
'version' => $package['version'] ?? '',
|
||||
'download_url' => $package['download_url'] ?? '',
|
||||
'homepage' => $package['homepage'] ?? '',
|
||||
'description' => $package['description'] ?? '',
|
||||
'author' => $package['author'] ?? '',
|
||||
'requires' => $package['requires'] ?? '',
|
||||
'requires_php' => $package['requires_php'] ?? '',
|
||||
'tested' => $package['tested'] ?? '',
|
||||
'signature' => $package['signature'] ?? null,
|
||||
'signature_valid' => null,
|
||||
'last_updated' => $package['last_updated'] ?? '',
|
||||
);
|
||||
}
|
||||
return [
|
||||
'did' => $package['did'] ?? '',
|
||||
'slug' => $package['slug'] ?? '',
|
||||
'name' => $package['name'] ?? '',
|
||||
'version' => $package['version'] ?? '',
|
||||
'download_url' => $package['download_url'] ?? '',
|
||||
'homepage' => $package['homepage'] ?? '',
|
||||
'description' => $package['description'] ?? '',
|
||||
'author' => $package['author'] ?? '',
|
||||
'requires' => $package['requires'] ?? '',
|
||||
'requires_php' => $package['requires_php'] ?? '',
|
||||
'tested' => $package['tested'] ?? '',
|
||||
'signature' => $package['signature'] ?? null,
|
||||
'signature_valid' => null,
|
||||
'last_updated' => $package['last_updated'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 sodium 扩展是否可用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_sodium_available(): bool {
|
||||
return function_exists( 'sodium_crypto_sign_verify_detached' ) ||
|
||||
class_exists( '\ParagonIE_Sodium_Compat' );
|
||||
}
|
||||
/**
|
||||
* 检查 sodium 扩展是否可用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_sodium_available(): bool {
|
||||
return function_exists( 'sodium_crypto_sign_verify_detached' ) ||
|
||||
class_exists( '\ParagonIE_Sodium_Compat' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的签名算法
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_supported_algorithms(): array {
|
||||
$algorithms = array();
|
||||
/**
|
||||
* 获取支持的签名算法
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_supported_algorithms(): array {
|
||||
$algorithms = [];
|
||||
|
||||
if ( $this->is_sodium_available() ) {
|
||||
$algorithms[] = self::SIGNATURE_ED25519;
|
||||
}
|
||||
if ( $this->is_sodium_available() ) {
|
||||
$algorithms[] = self::SIGNATURE_ED25519;
|
||||
}
|
||||
|
||||
return $algorithms;
|
||||
}
|
||||
return $algorithms;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace WPBridge\FAIR;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
use WPBridge\Core\SourceRegistry;
|
||||
|
|
@ -22,335 +22,335 @@ use WPBridge\Core\SourceRegistry;
|
|||
*/
|
||||
class FairSourceAdapter {
|
||||
|
||||
/**
|
||||
* FAIR 协议处理器
|
||||
*
|
||||
* @var FairProtocol
|
||||
*/
|
||||
private FairProtocol $protocol;
|
||||
/**
|
||||
* FAIR 协议处理器
|
||||
*
|
||||
* @var FairProtocol
|
||||
*/
|
||||
private FairProtocol $protocol;
|
||||
|
||||
/**
|
||||
* 源配置
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $source;
|
||||
/**
|
||||
* 源配置
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $source;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param array $source 源配置
|
||||
*/
|
||||
public function __construct( array $source ) {
|
||||
$this->source = $source;
|
||||
$this->protocol = new FairProtocol();
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param array $source 源配置
|
||||
*/
|
||||
public function __construct( array $source ) {
|
||||
$this->source = $source;
|
||||
$this->protocol = new FairProtocol();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件更新
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $version 当前版本
|
||||
* @return array|null 更新信息
|
||||
*/
|
||||
public function check_plugin_update( string $slug, string $version ): ?array {
|
||||
return $this->check_update( 'plugin', $slug, $version );
|
||||
}
|
||||
/**
|
||||
* 检查插件更新
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $version 当前版本
|
||||
* @return array|null 更新信息
|
||||
*/
|
||||
public function check_plugin_update( string $slug, string $version ): ?array {
|
||||
return $this->check_update( 'plugin', $slug, $version );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查主题更新
|
||||
*
|
||||
* @param string $slug 主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return array|null 更新信息
|
||||
*/
|
||||
public function check_theme_update( string $slug, string $version ): ?array {
|
||||
return $this->check_update( 'theme', $slug, $version );
|
||||
}
|
||||
/**
|
||||
* 检查主题更新
|
||||
*
|
||||
* @param string $slug 主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return array|null 更新信息
|
||||
*/
|
||||
public function check_theme_update( string $slug, string $version ): ?array {
|
||||
return $this->check_update( 'theme', $slug, $version );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $type 类型 (plugin/theme)
|
||||
* @param string $slug slug
|
||||
* @param string $version 当前版本
|
||||
* @return array|null
|
||||
*/
|
||||
private function check_update( string $type, string $slug, string $version ): ?array {
|
||||
$api_url = $this->source['api_url'] ?? '';
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $type 类型 (plugin/theme)
|
||||
* @param string $slug slug
|
||||
* @param string $version 当前版本
|
||||
* @return array|null
|
||||
*/
|
||||
private function check_update( string $type, string $slug, string $version ): ?array {
|
||||
$api_url = $this->source['api_url'] ?? '';
|
||||
|
||||
if ( empty( $api_url ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $api_url ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建 API 请求 URL
|
||||
$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug;
|
||||
// 构建 API 请求 URL
|
||||
$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug;
|
||||
|
||||
// 发送请求
|
||||
$response = $this->make_request( $endpoint );
|
||||
// 发送请求
|
||||
$response = $this->make_request( $endpoint );
|
||||
|
||||
if ( ! $response ) {
|
||||
return null;
|
||||
}
|
||||
if ( ! $response ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
$package = $this->parse_response( $response );
|
||||
// 解析响应
|
||||
$package = $this->parse_response( $response );
|
||||
|
||||
if ( ! $package ) {
|
||||
return null;
|
||||
}
|
||||
if ( ! $package ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查版本
|
||||
if ( version_compare( $package['version'], $version, '<=' ) ) {
|
||||
return null; // 没有更新
|
||||
}
|
||||
// 检查版本
|
||||
if ( version_compare( $package['version'], $version, '<=' ) ) {
|
||||
return null; // 没有更新
|
||||
}
|
||||
|
||||
// 验证签名(如果需要)
|
||||
if ( ! empty( $this->source['signature_required'] ) ) {
|
||||
$verification = $this->protocol->verify_package_signature( $response );
|
||||
// 验证签名(如果需要)
|
||||
if ( ! empty( $this->source['signature_required'] ) ) {
|
||||
$verification = $this->protocol->verify_package_signature( $response );
|
||||
|
||||
if ( ! $verification['valid'] ) {
|
||||
// 签名验证失败
|
||||
return null;
|
||||
}
|
||||
if ( ! $verification['valid'] ) {
|
||||
// 签名验证失败
|
||||
return null;
|
||||
}
|
||||
|
||||
$package['signature_valid'] = true;
|
||||
}
|
||||
$package['signature_valid'] = true;
|
||||
}
|
||||
|
||||
return $package;
|
||||
}
|
||||
return $package;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件信息
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_plugin_info( string $slug ): ?array {
|
||||
return $this->get_info( 'plugin', $slug );
|
||||
}
|
||||
/**
|
||||
* 获取插件信息
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_plugin_info( string $slug ): ?array {
|
||||
return $this->get_info( 'plugin', $slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主题信息
|
||||
*
|
||||
* @param string $slug 主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_theme_info( string $slug ): ?array {
|
||||
return $this->get_info( 'theme', $slug );
|
||||
}
|
||||
/**
|
||||
* 获取主题信息
|
||||
*
|
||||
* @param string $slug 主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_theme_info( string $slug ): ?array {
|
||||
return $this->get_info( 'theme', $slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @param string $slug slug
|
||||
* @return array|null
|
||||
*/
|
||||
private function get_info( string $type, string $slug ): ?array {
|
||||
$api_url = $this->source['api_url'] ?? '';
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @param string $slug slug
|
||||
* @return array|null
|
||||
*/
|
||||
private function get_info( string $type, string $slug ): ?array {
|
||||
$api_url = $this->source['api_url'] ?? '';
|
||||
|
||||
if ( empty( $api_url ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $api_url ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug . '/info';
|
||||
$response = $this->make_request( $endpoint );
|
||||
$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug . '/info';
|
||||
$response = $this->make_request( $endpoint );
|
||||
|
||||
if ( ! $response ) {
|
||||
return null;
|
||||
}
|
||||
if ( ! $response ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->parse_response( $response );
|
||||
}
|
||||
return $this->parse_response( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 DID 查询
|
||||
*
|
||||
* @param string $did DID 字符串
|
||||
* @return array|null
|
||||
*/
|
||||
public function query_by_did( string $did ): ?array {
|
||||
$parsed = $this->protocol->parse_did( $did );
|
||||
/**
|
||||
* 通过 DID 查询
|
||||
*
|
||||
* @param string $did DID 字符串
|
||||
* @return array|null
|
||||
*/
|
||||
public function query_by_did( string $did ): ?array {
|
||||
$parsed = $this->protocol->parse_did( $did );
|
||||
|
||||
if ( ! $parsed ) {
|
||||
return null;
|
||||
}
|
||||
if ( ! $parsed ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$api_url = $this->source['api_url'] ?? '';
|
||||
$api_url = $this->source['api_url'] ?? '';
|
||||
|
||||
if ( empty( $api_url ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $api_url ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// FAIR API 支持 DID 查询
|
||||
$endpoint = trailingslashit( $api_url ) . 'resolve?did=' . urlencode( $did );
|
||||
$response = $this->make_request( $endpoint );
|
||||
// FAIR API 支持 DID 查询
|
||||
$endpoint = trailingslashit( $api_url ) . 'resolve?did=' . urlencode( $did );
|
||||
$response = $this->make_request( $endpoint );
|
||||
|
||||
if ( ! $response ) {
|
||||
return null;
|
||||
}
|
||||
if ( ! $response ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->parse_response( $response );
|
||||
}
|
||||
return $this->parse_response( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 HTTP 请求
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return array|null
|
||||
*/
|
||||
private function make_request( string $url ): ?array {
|
||||
$args = array(
|
||||
'timeout' => 15,
|
||||
'user-agent' => 'WPBridge/' . WPBRIDGE_VERSION . ' WordPress/' . get_bloginfo( 'version' ),
|
||||
'headers' => array(
|
||||
'Accept' => 'application/json',
|
||||
),
|
||||
);
|
||||
/**
|
||||
* 发送 HTTP 请求
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return array|null
|
||||
*/
|
||||
private function make_request( string $url ): ?array {
|
||||
$args = [
|
||||
'timeout' => 15,
|
||||
'user-agent' => 'WPBridge/' . WPBRIDGE_VERSION . ' WordPress/' . get_bloginfo( 'version' ),
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
];
|
||||
|
||||
// 添加认证
|
||||
if ( ! empty( $this->source['auth_type'] ) && $this->source['auth_type'] !== SourceRegistry::AUTH_NONE ) {
|
||||
$auth_header = $this->get_auth_header();
|
||||
if ( $auth_header ) {
|
||||
$args['headers']['Authorization'] = $auth_header;
|
||||
}
|
||||
}
|
||||
// 添加认证
|
||||
if ( ! empty( $this->source['auth_type'] ) && $this->source['auth_type'] !== SourceRegistry::AUTH_NONE ) {
|
||||
$auth_header = $this->get_auth_header();
|
||||
if ( $auth_header ) {
|
||||
$args['headers']['Authorization'] = $auth_header;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加自定义头
|
||||
if ( ! empty( $this->source['headers'] ) && is_array( $this->source['headers'] ) ) {
|
||||
$args['headers'] = array_merge( $args['headers'], $this->source['headers'] );
|
||||
}
|
||||
// 添加自定义头
|
||||
if ( ! empty( $this->source['headers'] ) && is_array( $this->source['headers'] ) ) {
|
||||
$args['headers'] = array_merge( $args['headers'], $this->source['headers'] );
|
||||
}
|
||||
|
||||
$response = wp_remote_get( $url, $args );
|
||||
$response = wp_remote_get( $url, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( 'WPBridge FAIR request failed: ' . $response->get_error_message() . ' URL: ' . $url );
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if ( is_wp_error( $response ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( 'WPBridge FAIR request failed: ' . $response->get_error_message() . ' URL: ' . $url );
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( $code !== 200 ) {
|
||||
return null;
|
||||
}
|
||||
if ( $code !== 200 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
return null;
|
||||
}
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认证头
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function get_auth_header(): ?string {
|
||||
$auth_type = $this->source['auth_type'] ?? '';
|
||||
$secret_ref = $this->source['auth_secret_ref'] ?? '';
|
||||
/**
|
||||
* 获取认证头
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function get_auth_header(): ?string {
|
||||
$auth_type = $this->source['auth_type'] ?? '';
|
||||
$secret_ref = $this->source['auth_secret_ref'] ?? '';
|
||||
|
||||
if ( empty( $secret_ref ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $secret_ref ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取密钥
|
||||
$secret = get_option( 'wpbridge_secret_' . $secret_ref, '' );
|
||||
// 获取密钥
|
||||
$secret = get_option( 'wpbridge_secret_' . $secret_ref, '' );
|
||||
|
||||
if ( empty( $secret ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $secret ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch ( $auth_type ) {
|
||||
case SourceRegistry::AUTH_BEARER:
|
||||
return 'Bearer ' . $secret;
|
||||
switch ( $auth_type ) {
|
||||
case SourceRegistry::AUTH_BEARER:
|
||||
return 'Bearer ' . $secret;
|
||||
|
||||
case SourceRegistry::AUTH_TOKEN:
|
||||
return 'Token ' . $secret;
|
||||
case SourceRegistry::AUTH_TOKEN:
|
||||
return 'Token ' . $secret;
|
||||
|
||||
case SourceRegistry::AUTH_BASIC:
|
||||
return 'Basic ' . base64_encode( $secret );
|
||||
case SourceRegistry::AUTH_BASIC:
|
||||
return 'Basic ' . base64_encode( $secret );
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析响应
|
||||
*
|
||||
* @param array $response 响应数据
|
||||
* @return array|null
|
||||
*/
|
||||
private function parse_response( array $response ): ?array {
|
||||
// 标准 FAIR 响应格式
|
||||
if ( isset( $response['data'] ) ) {
|
||||
$response = $response['data'];
|
||||
}
|
||||
/**
|
||||
* 解析响应
|
||||
*
|
||||
* @param array $response 响应数据
|
||||
* @return array|null
|
||||
*/
|
||||
private function parse_response( array $response ): ?array {
|
||||
// 标准 FAIR 响应格式
|
||||
if ( isset( $response['data'] ) ) {
|
||||
$response = $response['data'];
|
||||
}
|
||||
|
||||
if ( empty( $response['slug'] ) && empty( $response['did'] ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $response['slug'] ) && empty( $response['did'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'did' => $response['did'] ?? '',
|
||||
'slug' => $response['slug'] ?? '',
|
||||
'name' => $response['name'] ?? '',
|
||||
'version' => $response['version'] ?? '',
|
||||
'new_version' => $response['version'] ?? '',
|
||||
'download_url' => $response['download_url'] ?? $response['download_link'] ?? '',
|
||||
'package' => $response['download_url'] ?? $response['download_link'] ?? '',
|
||||
'homepage' => $response['homepage'] ?? '',
|
||||
'url' => $response['homepage'] ?? '',
|
||||
'description' => $response['description'] ?? '',
|
||||
'author' => $response['author'] ?? '',
|
||||
'requires' => $response['requires'] ?? '',
|
||||
'requires_php' => $response['requires_php'] ?? '',
|
||||
'tested' => $response['tested'] ?? '',
|
||||
'last_updated' => $response['last_updated'] ?? '',
|
||||
'signature' => $response['signature'] ?? null,
|
||||
'signature_valid' => null,
|
||||
'icons' => $response['icons'] ?? array(),
|
||||
'banners' => $response['banners'] ?? array(),
|
||||
);
|
||||
}
|
||||
return [
|
||||
'did' => $response['did'] ?? '',
|
||||
'slug' => $response['slug'] ?? '',
|
||||
'name' => $response['name'] ?? '',
|
||||
'version' => $response['version'] ?? '',
|
||||
'new_version' => $response['version'] ?? '',
|
||||
'download_url' => $response['download_url'] ?? $response['download_link'] ?? '',
|
||||
'package' => $response['download_url'] ?? $response['download_link'] ?? '',
|
||||
'homepage' => $response['homepage'] ?? '',
|
||||
'url' => $response['homepage'] ?? '',
|
||||
'description' => $response['description'] ?? '',
|
||||
'author' => $response['author'] ?? '',
|
||||
'requires' => $response['requires'] ?? '',
|
||||
'requires_php' => $response['requires_php'] ?? '',
|
||||
'tested' => $response['tested'] ?? '',
|
||||
'last_updated' => $response['last_updated'] ?? '',
|
||||
'signature' => $response['signature'] ?? null,
|
||||
'signature_valid' => null,
|
||||
'icons' => $response['icons'] ?? [],
|
||||
'banners' => $response['banners'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证下载包
|
||||
*
|
||||
* @param string $file_path 文件路径
|
||||
* @param array $package 包信息
|
||||
* @return bool
|
||||
*/
|
||||
public function verify_download( string $file_path, array $package ): bool {
|
||||
// 如果不需要签名验证
|
||||
if ( empty( $this->source['signature_required'] ) ) {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 验证下载包
|
||||
*
|
||||
* @param string $file_path 文件路径
|
||||
* @param array $package 包信息
|
||||
* @return bool
|
||||
*/
|
||||
public function verify_download( string $file_path, array $package ): bool {
|
||||
// 如果不需要签名验证
|
||||
if ( empty( $this->source['signature_required'] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查包是否有签名
|
||||
if ( empty( $package['signature'] ) ) {
|
||||
return false;
|
||||
}
|
||||
// 检查包是否有签名
|
||||
if ( empty( $package['signature'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算文件哈希
|
||||
$file_hash = hash_file( 'sha256', $file_path );
|
||||
// 计算文件哈希
|
||||
$file_hash = hash_file( 'sha256', $file_path );
|
||||
|
||||
// 验证哈希签名
|
||||
$signature_data = $package['signature'];
|
||||
// 验证哈希签名
|
||||
$signature_data = $package['signature'];
|
||||
|
||||
if ( isset( $signature_data['file_hash'] ) ) {
|
||||
if ( $signature_data['file_hash'] !== $file_hash ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ( isset( $signature_data['file_hash'] ) ) {
|
||||
if ( $signature_data['file_hash'] !== $file_hash ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Core\Settings;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,118 +19,115 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class EmailHandler implements HandlerInterface {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 支持的通知类型
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $supported_types = array( 'update', 'error', 'recovery' );
|
||||
/**
|
||||
* 支持的通知类型
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $supported_types = [ 'update', 'error', 'recovery' ];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取处理器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'email';
|
||||
}
|
||||
/**
|
||||
* 获取处理器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'email';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool {
|
||||
$notification_settings = $this->settings->get( 'notifications', array() );
|
||||
return ! empty( $notification_settings['email']['enabled'] );
|
||||
}
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool {
|
||||
$notification_settings = $this->settings->get( 'notifications', [] );
|
||||
return ! empty( $notification_settings['email']['enabled'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否支持该通知类型
|
||||
*
|
||||
* @param string $type 通知类型
|
||||
* @return bool
|
||||
*/
|
||||
public function supports_type( string $type ): bool {
|
||||
$notification_settings = $this->settings->get( 'notifications', array() );
|
||||
$enabled_types = $notification_settings['email']['types'] ?? $this->supported_types;
|
||||
/**
|
||||
* 是否支持该通知类型
|
||||
*
|
||||
* @param string $type 通知类型
|
||||
* @return bool
|
||||
*/
|
||||
public function supports_type( string $type ): bool {
|
||||
$notification_settings = $this->settings->get( 'notifications', [] );
|
||||
$enabled_types = $notification_settings['email']['types'] ?? $this->supported_types;
|
||||
|
||||
return in_array( $type, $enabled_types, true );
|
||||
}
|
||||
return in_array( $type, $enabled_types, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @throws \Exception 发送失败时抛出异常
|
||||
*/
|
||||
public function send( string $subject, string $message, array $data = array() ): void {
|
||||
$notification_settings = $this->settings->get( 'notifications', array() );
|
||||
$recipients = $notification_settings['email']['recipients'] ?? array();
|
||||
/**
|
||||
* 发送通知
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @throws \Exception 发送失败时抛出异常
|
||||
*/
|
||||
public function send( string $subject, string $message, array $data = [] ): void {
|
||||
$notification_settings = $this->settings->get( 'notifications', [] );
|
||||
$recipients = $notification_settings['email']['recipients'] ?? [];
|
||||
|
||||
if ( empty( $recipients ) ) {
|
||||
// 默认发送给管理员
|
||||
$recipients = array( get_option( 'admin_email' ) );
|
||||
}
|
||||
if ( empty( $recipients ) ) {
|
||||
// 默认发送给管理员
|
||||
$recipients = [ get_option( 'admin_email' ) ];
|
||||
}
|
||||
|
||||
// 验证收件人邮箱格式
|
||||
$valid_recipients = array_filter(
|
||||
$recipients,
|
||||
function ( $email ) {
|
||||
return is_email( $email );
|
||||
}
|
||||
);
|
||||
// 验证收件人邮箱格式
|
||||
$valid_recipients = array_filter( $recipients, function ( $email ) {
|
||||
return is_email( $email );
|
||||
} );
|
||||
|
||||
if ( empty( $valid_recipients ) ) {
|
||||
throw new \Exception( __( '没有有效的收件人邮箱', 'wpbridge' ) );
|
||||
}
|
||||
if ( empty( $valid_recipients ) ) {
|
||||
throw new \Exception( __( '没有有效的收件人邮箱', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
// 构建 HTML 邮件
|
||||
$html_message = $this->build_html_message( $subject, $message, $data );
|
||||
// 构建 HTML 邮件
|
||||
$html_message = $this->build_html_message( $subject, $message, $data );
|
||||
|
||||
// 使用 WordPress 默认发件人,避免 SPF/DKIM 问题
|
||||
$headers = array(
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
);
|
||||
// 使用 WordPress 默认发件人,避免 SPF/DKIM 问题
|
||||
$headers = [
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
];
|
||||
|
||||
$sent = wp_mail( $valid_recipients, $subject, $html_message, $headers );
|
||||
$sent = wp_mail( $valid_recipients, $subject, $html_message, $headers );
|
||||
|
||||
if ( ! $sent ) {
|
||||
throw new \Exception( __( '邮件发送失败', 'wpbridge' ) );
|
||||
}
|
||||
}
|
||||
if ( ! $sent ) {
|
||||
throw new \Exception( __( '邮件发送失败', 'wpbridge' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 HTML 邮件内容
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @return string
|
||||
*/
|
||||
private function build_html_message( string $subject, string $message, array $data ): string {
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
$site_url = get_site_url();
|
||||
/**
|
||||
* 构建 HTML 邮件内容
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @return string
|
||||
*/
|
||||
private function build_html_message( string $subject, string $message, array $data ): string {
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
$site_url = get_site_url();
|
||||
|
||||
$html = '<!DOCTYPE html>
|
||||
$html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
|
@ -154,31 +151,31 @@ class EmailHandler implements HandlerInterface {
|
|||
<div class="content">
|
||||
<p>' . nl2br( esc_html( $message ) ) . '</p>';
|
||||
|
||||
// 添加数据表格
|
||||
if ( ! empty( $data ) && ! isset( $data['test'] ) ) {
|
||||
$html .= '<table class="data-table">';
|
||||
foreach ( $data as $key => $value ) {
|
||||
if ( is_scalar( $value ) ) {
|
||||
$html .= '<tr><th>' . esc_html( $key ) . '</th><td>' . esc_html( $value ) . '</td></tr>';
|
||||
}
|
||||
}
|
||||
$html .= '</table>';
|
||||
}
|
||||
// 添加数据表格
|
||||
if ( ! empty( $data ) && ! isset( $data['test'] ) ) {
|
||||
$html .= '<table class="data-table">';
|
||||
foreach ( $data as $key => $value ) {
|
||||
if ( is_scalar( $value ) ) {
|
||||
$html .= '<tr><th>' . esc_html( $key ) . '</th><td>' . esc_html( $value ) . '</td></tr>';
|
||||
}
|
||||
}
|
||||
$html .= '</table>';
|
||||
}
|
||||
|
||||
$html .= '
|
||||
$html .= '
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>' . sprintf(
|
||||
/* translators: %s: site name */
|
||||
esc_html__( '此邮件由 %s 的 WPBridge 插件发送', 'wpbridge' ),
|
||||
esc_html( $site_name )
|
||||
) . '</p>
|
||||
/* translators: %s: site name */
|
||||
esc_html__( '此邮件由 %s 的 WPBridge 插件发送', 'wpbridge' ),
|
||||
esc_html( $site_name )
|
||||
) . '</p>
|
||||
<p><a href="' . esc_url( $site_url ) . '">' . esc_html( $site_url ) . '</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\Notification;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,35 +17,35 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
interface HandlerInterface {
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @throws \Exception 发送失败时抛出异常
|
||||
*/
|
||||
public function send( string $subject, string $message, array $data = array() ): void;
|
||||
/**
|
||||
* 发送通知
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @throws \Exception 发送失败时抛出异常
|
||||
*/
|
||||
public function send( string $subject, string $message, array $data = [] ): void;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool;
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool;
|
||||
|
||||
/**
|
||||
* 是否支持该通知类型
|
||||
*
|
||||
* @param string $type 通知类型
|
||||
* @return bool
|
||||
*/
|
||||
public function supports_type( string $type ): bool;
|
||||
/**
|
||||
* 是否支持该通知类型
|
||||
*
|
||||
* @param string $type 通知类型
|
||||
* @return bool
|
||||
*/
|
||||
public function supports_type( string $type ): bool;
|
||||
|
||||
/**
|
||||
* 获取处理器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string;
|
||||
/**
|
||||
* 获取处理器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20,237 +20,212 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class NotificationManager {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 通知处理器
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $handlers = array();
|
||||
/**
|
||||
* 通知处理器
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $handlers = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->init_handlers();
|
||||
$this->init_hooks();
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->init_handlers();
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化处理器
|
||||
*/
|
||||
private function init_handlers(): void {
|
||||
$this->handlers = array(
|
||||
'email' => new EmailHandler( $this->settings ),
|
||||
'webhook' => new WebhookHandler( $this->settings ),
|
||||
);
|
||||
/**
|
||||
* 初始化处理器
|
||||
*/
|
||||
private function init_handlers(): void {
|
||||
$this->handlers = [
|
||||
'email' => new EmailHandler( $this->settings ),
|
||||
'webhook' => new WebhookHandler( $this->settings ),
|
||||
];
|
||||
|
||||
// 允许第三方扩展通知处理器
|
||||
$this->handlers = apply_filters(
|
||||
'wpbridge_notification_handlers',
|
||||
$this->handlers,
|
||||
$this->settings
|
||||
);
|
||||
}
|
||||
// 允许第三方扩展通知处理器
|
||||
$this->handlers = apply_filters(
|
||||
'wpbridge_notification_handlers',
|
||||
$this->handlers,
|
||||
$this->settings
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
add_action( 'wpbridge_update_available', array( $this, 'on_update_available' ), 10, 2 );
|
||||
add_action( 'wpbridge_source_error', array( $this, 'on_source_error' ), 10, 2 );
|
||||
add_action( 'wpbridge_source_recovered', array( $this, 'on_source_recovered' ), 10, 1 );
|
||||
}
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
add_action( 'wpbridge_update_available', [ $this, 'on_update_available' ], 10, 2 );
|
||||
add_action( 'wpbridge_source_error', [ $this, 'on_source_error' ], 10, 2 );
|
||||
add_action( 'wpbridge_source_recovered', [ $this, 'on_source_recovered' ], 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
*
|
||||
* @param string $type 通知类型
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
*/
|
||||
public function send( string $type, string $subject, string $message, array $data = array() ): void {
|
||||
// 速率限制检查:防止通知轰炸
|
||||
$rate_limit_key = 'wpbridge_notification_' . md5( $type . $subject );
|
||||
if ( get_transient( $rate_limit_key ) ) {
|
||||
Logger::debug(
|
||||
'通知被速率限制',
|
||||
array(
|
||||
'type' => $type,
|
||||
'subject' => $subject,
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 发送通知
|
||||
*
|
||||
* @param string $type 通知类型
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
*/
|
||||
public function send( string $type, string $subject, string $message, array $data = [] ): void {
|
||||
// 速率限制检查:防止通知轰炸
|
||||
$rate_limit_key = 'wpbridge_notification_' . md5( $type . $subject );
|
||||
if ( get_transient( $rate_limit_key ) ) {
|
||||
Logger::debug( '通知被速率限制', [ 'type' => $type, 'subject' => $subject ] );
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置 5 分钟冷却时间
|
||||
set_transient( $rate_limit_key, true, 5 * MINUTE_IN_SECONDS );
|
||||
// 设置 5 分钟冷却时间
|
||||
set_transient( $rate_limit_key, true, 5 * MINUTE_IN_SECONDS );
|
||||
|
||||
foreach ( $this->handlers as $name => $handler ) {
|
||||
if ( $handler->is_enabled() && $handler->supports_type( $type ) ) {
|
||||
try {
|
||||
$handler->send( $subject, $message, $data );
|
||||
Logger::debug(
|
||||
'通知发送成功',
|
||||
array(
|
||||
'handler' => $name,
|
||||
'type' => $type,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
Logger::error(
|
||||
'通知发送失败',
|
||||
array(
|
||||
'handler' => $name,
|
||||
'error' => $e->getMessage(),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ( $this->handlers as $name => $handler ) {
|
||||
if ( $handler->is_enabled() && $handler->supports_type( $type ) ) {
|
||||
try {
|
||||
$handler->send( $subject, $message, $data );
|
||||
Logger::debug( '通知发送成功', [
|
||||
'handler' => $name,
|
||||
'type' => $type,
|
||||
] );
|
||||
} catch ( \Exception $e ) {
|
||||
Logger::error( '通知发送失败', [
|
||||
'handler' => $name,
|
||||
'error' => $e->getMessage(),
|
||||
] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新可用时触发
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param array $update 更新信息
|
||||
*/
|
||||
public function on_update_available( string $slug, array $update ): void {
|
||||
$subject = sprintf(
|
||||
/* translators: %s: plugin/theme name */
|
||||
__( '[WPBridge] %s 有新版本可用', 'wpbridge' ),
|
||||
$update['name'] ?? $slug
|
||||
);
|
||||
/**
|
||||
* 更新可用时触发
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param array $update 更新信息
|
||||
*/
|
||||
public function on_update_available( string $slug, array $update ): void {
|
||||
$subject = sprintf(
|
||||
/* translators: %s: plugin/theme name */
|
||||
__( '[WPBridge] %s 有新版本可用', 'wpbridge' ),
|
||||
$update['name'] ?? $slug
|
||||
);
|
||||
|
||||
$message = sprintf(
|
||||
/* translators: 1: name, 2: current version, 3: new version */
|
||||
__( '%1$s 有新版本可用。当前版本: %2$s,新版本: %3$s', 'wpbridge' ),
|
||||
$update['name'] ?? $slug,
|
||||
$update['current_version'] ?? 'unknown',
|
||||
$update['new_version'] ?? 'unknown'
|
||||
);
|
||||
$message = sprintf(
|
||||
/* translators: 1: name, 2: current version, 3: new version */
|
||||
__( '%1$s 有新版本可用。当前版本: %2$s,新版本: %3$s', 'wpbridge' ),
|
||||
$update['name'] ?? $slug,
|
||||
$update['current_version'] ?? 'unknown',
|
||||
$update['new_version'] ?? 'unknown'
|
||||
);
|
||||
|
||||
$this->send( 'update', $subject, $message, $update );
|
||||
}
|
||||
$this->send( 'update', $subject, $message, $update );
|
||||
}
|
||||
|
||||
/**
|
||||
* 源错误时触发
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param string $error 错误信息
|
||||
*/
|
||||
public function on_source_error( string $source_id, string $error ): void {
|
||||
$subject = sprintf(
|
||||
/* translators: %s: source ID */
|
||||
__( '[WPBridge] 更新源 %s 出现错误', 'wpbridge' ),
|
||||
$source_id
|
||||
);
|
||||
/**
|
||||
* 源错误时触发
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param string $error 错误信息
|
||||
*/
|
||||
public function on_source_error( string $source_id, string $error ): void {
|
||||
$subject = sprintf(
|
||||
/* translators: %s: source ID */
|
||||
__( '[WPBridge] 更新源 %s 出现错误', 'wpbridge' ),
|
||||
$source_id
|
||||
);
|
||||
|
||||
$message = sprintf(
|
||||
/* translators: 1: source ID, 2: error message */
|
||||
__( '更新源 %1$s 出现错误: %2$s', 'wpbridge' ),
|
||||
$source_id,
|
||||
$error
|
||||
);
|
||||
$message = sprintf(
|
||||
/* translators: 1: source ID, 2: error message */
|
||||
__( '更新源 %1$s 出现错误: %2$s', 'wpbridge' ),
|
||||
$source_id,
|
||||
$error
|
||||
);
|
||||
|
||||
$this->send(
|
||||
'error',
|
||||
$subject,
|
||||
$message,
|
||||
array(
|
||||
'source_id' => $source_id,
|
||||
'error' => $error,
|
||||
)
|
||||
);
|
||||
}
|
||||
$this->send( 'error', $subject, $message, [
|
||||
'source_id' => $source_id,
|
||||
'error' => $error,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 源恢复时触发
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
public function on_source_recovered( string $source_id ): void {
|
||||
$subject = sprintf(
|
||||
/* translators: %s: source ID */
|
||||
__( '[WPBridge] 更新源 %s 已恢复', 'wpbridge' ),
|
||||
$source_id
|
||||
);
|
||||
/**
|
||||
* 源恢复时触发
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
public function on_source_recovered( string $source_id ): void {
|
||||
$subject = sprintf(
|
||||
/* translators: %s: source ID */
|
||||
__( '[WPBridge] 更新源 %s 已恢复', 'wpbridge' ),
|
||||
$source_id
|
||||
);
|
||||
|
||||
$message = sprintf(
|
||||
/* translators: %s: source ID */
|
||||
__( '更新源 %s 已恢复正常', 'wpbridge' ),
|
||||
$source_id
|
||||
);
|
||||
$message = sprintf(
|
||||
/* translators: %s: source ID */
|
||||
__( '更新源 %s 已恢复正常', 'wpbridge' ),
|
||||
$source_id
|
||||
);
|
||||
|
||||
$this->send(
|
||||
'recovery',
|
||||
$subject,
|
||||
$message,
|
||||
array(
|
||||
'source_id' => $source_id,
|
||||
)
|
||||
);
|
||||
}
|
||||
$this->send( 'recovery', $subject, $message, [
|
||||
'source_id' => $source_id,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取处理器
|
||||
*
|
||||
* @param string $name 处理器名称
|
||||
* @return HandlerInterface|null
|
||||
*/
|
||||
public function get_handler( string $name ): ?HandlerInterface {
|
||||
return $this->handlers[ $name ] ?? null;
|
||||
}
|
||||
/**
|
||||
* 获取处理器
|
||||
*
|
||||
* @param string $name 处理器名称
|
||||
* @return HandlerInterface|null
|
||||
*/
|
||||
public function get_handler( string $name ): ?HandlerInterface {
|
||||
return $this->handlers[ $name ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有处理器
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_handlers(): array {
|
||||
return $this->handlers;
|
||||
}
|
||||
/**
|
||||
* 获取所有处理器
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_handlers(): array {
|
||||
return $this->handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试通知
|
||||
*
|
||||
* @param string $handler_name 处理器名称
|
||||
* @return bool
|
||||
*/
|
||||
public function test( string $handler_name ): bool {
|
||||
$handler = $this->get_handler( $handler_name );
|
||||
/**
|
||||
* 测试通知
|
||||
*
|
||||
* @param string $handler_name 处理器名称
|
||||
* @return bool
|
||||
*/
|
||||
public function test( string $handler_name ): bool {
|
||||
$handler = $this->get_handler( $handler_name );
|
||||
|
||||
if ( null === $handler ) {
|
||||
return false;
|
||||
}
|
||||
if ( null === $handler ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$handler->send(
|
||||
__( '[WPBridge] 测试通知', 'wpbridge' ),
|
||||
__( '这是一条测试通知,如果您收到此消息,说明通知配置正确。', 'wpbridge' ),
|
||||
array( 'test' => true )
|
||||
);
|
||||
return true;
|
||||
} catch ( \Exception $e ) {
|
||||
Logger::error(
|
||||
'测试通知失败',
|
||||
array(
|
||||
'handler' => $handler_name,
|
||||
'error' => $e->getMessage(),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
$handler->send(
|
||||
__( '[WPBridge] 测试通知', 'wpbridge' ),
|
||||
__( '这是一条测试通知,如果您收到此消息,说明通知配置正确。', 'wpbridge' ),
|
||||
[ 'test' => true ]
|
||||
);
|
||||
return true;
|
||||
} catch ( \Exception $e ) {
|
||||
Logger::error( '测试通知失败', [
|
||||
'handler' => $handler_name,
|
||||
'error' => $e->getMessage(),
|
||||
] );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use WPBridge\Security\Validator;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -21,306 +21,300 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class WebhookHandler implements HandlerInterface {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 支持的通知类型
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $supported_types = array( 'update', 'error', 'recovery' );
|
||||
/**
|
||||
* 支持的通知类型
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $supported_types = [ 'update', 'error', 'recovery' ];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取处理器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'webhook';
|
||||
}
|
||||
/**
|
||||
* 获取处理器名称
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'webhook';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool {
|
||||
$notification_settings = $this->settings->get( 'notifications', array() );
|
||||
return ! empty( $notification_settings['webhook']['enabled'] ) &&
|
||||
! empty( $notification_settings['webhook']['url'] );
|
||||
}
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_enabled(): bool {
|
||||
$notification_settings = $this->settings->get( 'notifications', [] );
|
||||
return ! empty( $notification_settings['webhook']['enabled'] ) &&
|
||||
! empty( $notification_settings['webhook']['url'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否支持该通知类型
|
||||
*
|
||||
* @param string $type 通知类型
|
||||
* @return bool
|
||||
*/
|
||||
public function supports_type( string $type ): bool {
|
||||
$notification_settings = $this->settings->get( 'notifications', array() );
|
||||
$enabled_types = $notification_settings['webhook']['types'] ?? $this->supported_types;
|
||||
/**
|
||||
* 是否支持该通知类型
|
||||
*
|
||||
* @param string $type 通知类型
|
||||
* @return bool
|
||||
*/
|
||||
public function supports_type( string $type ): bool {
|
||||
$notification_settings = $this->settings->get( 'notifications', [] );
|
||||
$enabled_types = $notification_settings['webhook']['types'] ?? $this->supported_types;
|
||||
|
||||
return in_array( $type, $enabled_types, true );
|
||||
}
|
||||
return in_array( $type, $enabled_types, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @throws \Exception 发送失败时抛出异常
|
||||
*/
|
||||
public function send( string $subject, string $message, array $data = array() ): void {
|
||||
$notification_settings = $this->settings->get( 'notifications', array() );
|
||||
$webhook_url = $notification_settings['webhook']['url'] ?? '';
|
||||
$webhook_secret = $notification_settings['webhook']['secret'] ?? '';
|
||||
$webhook_format = $notification_settings['webhook']['format'] ?? 'json';
|
||||
/**
|
||||
* 发送通知
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @throws \Exception 发送失败时抛出异常
|
||||
*/
|
||||
public function send( string $subject, string $message, array $data = [] ): void {
|
||||
$notification_settings = $this->settings->get( 'notifications', [] );
|
||||
$webhook_url = $notification_settings['webhook']['url'] ?? '';
|
||||
$webhook_secret = $notification_settings['webhook']['secret'] ?? '';
|
||||
$webhook_format = $notification_settings['webhook']['format'] ?? 'json';
|
||||
|
||||
if ( empty( $webhook_url ) ) {
|
||||
throw new \Exception( __( 'Webhook URL 未配置', 'wpbridge' ) );
|
||||
}
|
||||
if ( empty( $webhook_url ) ) {
|
||||
throw new \Exception( __( 'Webhook URL 未配置', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
// SSRF 防护:验证 URL 安全性
|
||||
if ( ! Validator::is_valid_url( $webhook_url ) ) {
|
||||
throw new \Exception( __( 'Webhook URL 不安全,禁止访问内网地址', 'wpbridge' ) );
|
||||
}
|
||||
// SSRF 防护:验证 URL 安全性
|
||||
if ( ! Validator::is_valid_url( $webhook_url ) ) {
|
||||
throw new \Exception( __( 'Webhook URL 不安全,禁止访问内网地址', 'wpbridge' ) );
|
||||
}
|
||||
|
||||
// 构建 payload
|
||||
$payload = $this->build_payload( $subject, $message, $data, $webhook_format );
|
||||
// 构建 payload
|
||||
$payload = $this->build_payload( $subject, $message, $data, $webhook_format );
|
||||
|
||||
// 构建请求头
|
||||
$headers = array(
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'WPBridge/' . WPBRIDGE_VERSION,
|
||||
);
|
||||
// 构建请求头
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'WPBridge/' . WPBRIDGE_VERSION,
|
||||
];
|
||||
|
||||
// 添加签名
|
||||
if ( ! empty( $webhook_secret ) ) {
|
||||
$signature = $this->generate_signature( $payload, $webhook_secret );
|
||||
$headers['X-WPBridge-Signature'] = $signature;
|
||||
}
|
||||
// 添加签名
|
||||
if ( ! empty( $webhook_secret ) ) {
|
||||
$signature = $this->generate_signature( $payload, $webhook_secret );
|
||||
$headers['X-WPBridge-Signature'] = $signature;
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
$response = wp_remote_post(
|
||||
$webhook_url,
|
||||
array(
|
||||
'headers' => $headers,
|
||||
'body' => $payload,
|
||||
'timeout' => 10,
|
||||
)
|
||||
);
|
||||
// 发送请求
|
||||
$response = wp_remote_post( $webhook_url, [
|
||||
'headers' => $headers,
|
||||
'body' => $payload,
|
||||
'timeout' => 10,
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
throw new \Exception( $response->get_error_message() );
|
||||
}
|
||||
if ( is_wp_error( $response ) ) {
|
||||
throw new \Exception( $response->get_error_message() );
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( $status_code < 200 || $status_code >= 300 ) {
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
/* translators: %d: HTTP status code */
|
||||
__( 'Webhook 返回错误状态码: %d', 'wpbridge' ),
|
||||
$status_code
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $status_code < 200 || $status_code >= 300 ) {
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
/* translators: %d: HTTP status code */
|
||||
__( 'Webhook 返回错误状态码: %d', 'wpbridge' ),
|
||||
$status_code
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Logger::debug(
|
||||
'Webhook 发送成功',
|
||||
array(
|
||||
'url' => $webhook_url,
|
||||
'status_code' => $status_code,
|
||||
)
|
||||
);
|
||||
}
|
||||
Logger::debug( 'Webhook 发送成功', [
|
||||
'url' => $webhook_url,
|
||||
'status_code' => $status_code,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 payload
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @param string $format 格式
|
||||
* @return string
|
||||
*/
|
||||
private function build_payload( string $subject, string $message, array $data, string $format ): string {
|
||||
$base_payload = array(
|
||||
'event' => $data['type'] ?? 'notification',
|
||||
'subject' => $subject,
|
||||
'message' => $message,
|
||||
'timestamp' => current_time( 'c' ),
|
||||
'site' => array(
|
||||
'name' => get_bloginfo( 'name' ),
|
||||
'url' => get_site_url(),
|
||||
),
|
||||
'data' => $data,
|
||||
);
|
||||
/**
|
||||
* 构建 payload
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @param string $format 格式
|
||||
* @return string
|
||||
*/
|
||||
private function build_payload( string $subject, string $message, array $data, string $format ): string {
|
||||
$base_payload = [
|
||||
'event' => $data['type'] ?? 'notification',
|
||||
'subject' => $subject,
|
||||
'message' => $message,
|
||||
'timestamp' => current_time( 'c' ),
|
||||
'site' => [
|
||||
'name' => get_bloginfo( 'name' ),
|
||||
'url' => get_site_url(),
|
||||
],
|
||||
'data' => $data,
|
||||
];
|
||||
|
||||
switch ( $format ) {
|
||||
case 'slack':
|
||||
return $this->format_slack( $subject, $message, $data );
|
||||
switch ( $format ) {
|
||||
case 'slack':
|
||||
return $this->format_slack( $subject, $message, $data );
|
||||
|
||||
case 'discord':
|
||||
return $this->format_discord( $subject, $message, $data );
|
||||
case 'discord':
|
||||
return $this->format_discord( $subject, $message, $data );
|
||||
|
||||
case 'teams':
|
||||
return $this->format_teams( $subject, $message, $data );
|
||||
case 'teams':
|
||||
return $this->format_teams( $subject, $message, $data );
|
||||
|
||||
default:
|
||||
return wp_json_encode( $base_payload );
|
||||
}
|
||||
}
|
||||
default:
|
||||
return wp_json_encode( $base_payload );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Slack 格式
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @return string
|
||||
*/
|
||||
private function format_slack( string $subject, string $message, array $data ): string {
|
||||
$payload = array(
|
||||
'text' => $subject,
|
||||
'attachments' => array(
|
||||
array(
|
||||
'color' => $this->get_color_for_type( $data['type'] ?? 'info' ),
|
||||
'text' => $message,
|
||||
'footer' => 'WPBridge | ' . get_site_url(),
|
||||
'ts' => time(),
|
||||
),
|
||||
),
|
||||
);
|
||||
/**
|
||||
* Slack 格式
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @return string
|
||||
*/
|
||||
private function format_slack( string $subject, string $message, array $data ): string {
|
||||
$payload = [
|
||||
'text' => $subject,
|
||||
'attachments' => [
|
||||
[
|
||||
'color' => $this->get_color_for_type( $data['type'] ?? 'info' ),
|
||||
'text' => $message,
|
||||
'footer' => 'WPBridge | ' . get_site_url(),
|
||||
'ts' => time(),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return wp_json_encode( $payload );
|
||||
}
|
||||
return wp_json_encode( $payload );
|
||||
}
|
||||
|
||||
/**
|
||||
* Discord 格式
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @return string
|
||||
*/
|
||||
private function format_discord( string $subject, string $message, array $data ): string {
|
||||
$payload = array(
|
||||
'embeds' => array(
|
||||
array(
|
||||
'title' => $subject,
|
||||
'description' => $message,
|
||||
'color' => $this->get_color_int_for_type( $data['type'] ?? 'info' ),
|
||||
'footer' => array(
|
||||
'text' => 'WPBridge | ' . get_site_url(),
|
||||
),
|
||||
'timestamp' => gmdate( 'c' ),
|
||||
),
|
||||
),
|
||||
);
|
||||
/**
|
||||
* Discord 格式
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @return string
|
||||
*/
|
||||
private function format_discord( string $subject, string $message, array $data ): string {
|
||||
$payload = [
|
||||
'embeds' => [
|
||||
[
|
||||
'title' => $subject,
|
||||
'description' => $message,
|
||||
'color' => $this->get_color_int_for_type( $data['type'] ?? 'info' ),
|
||||
'footer' => [
|
||||
'text' => 'WPBridge | ' . get_site_url(),
|
||||
],
|
||||
'timestamp' => gmdate( 'c' ),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return wp_json_encode( $payload );
|
||||
}
|
||||
return wp_json_encode( $payload );
|
||||
}
|
||||
|
||||
/**
|
||||
* Microsoft Teams 格式
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @return string
|
||||
*/
|
||||
private function format_teams( string $subject, string $message, array $data ): string {
|
||||
$payload = array(
|
||||
'@type' => 'MessageCard',
|
||||
'@context' => 'http://schema.org/extensions',
|
||||
'themeColor' => $this->get_color_hex_for_type( $data['type'] ?? 'info' ),
|
||||
'summary' => $subject,
|
||||
'sections' => array(
|
||||
array(
|
||||
'activityTitle' => $subject,
|
||||
'text' => $message,
|
||||
),
|
||||
),
|
||||
);
|
||||
/**
|
||||
* Microsoft Teams 格式
|
||||
*
|
||||
* @param string $subject 主题
|
||||
* @param string $message 消息
|
||||
* @param array $data 附加数据
|
||||
* @return string
|
||||
*/
|
||||
private function format_teams( string $subject, string $message, array $data ): string {
|
||||
$payload = [
|
||||
'@type' => 'MessageCard',
|
||||
'@context' => 'http://schema.org/extensions',
|
||||
'themeColor' => $this->get_color_hex_for_type( $data['type'] ?? 'info' ),
|
||||
'summary' => $subject,
|
||||
'sections' => [
|
||||
[
|
||||
'activityTitle' => $subject,
|
||||
'text' => $message,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return wp_json_encode( $payload );
|
||||
}
|
||||
return wp_json_encode( $payload );
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*
|
||||
* @param string $payload 负载
|
||||
* @param string $secret 密钥
|
||||
* @return string
|
||||
*/
|
||||
private function generate_signature( string $payload, string $secret ): string {
|
||||
return 'sha256=' . hash_hmac( 'sha256', $payload, $secret );
|
||||
}
|
||||
/**
|
||||
* 生成签名
|
||||
*
|
||||
* @param string $payload 负载
|
||||
* @param string $secret 密钥
|
||||
* @return string
|
||||
*/
|
||||
private function generate_signature( string $payload, string $secret ): string {
|
||||
return 'sha256=' . hash_hmac( 'sha256', $payload, $secret );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型对应的颜色(Slack 格式)
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return string
|
||||
*/
|
||||
private function get_color_for_type( string $type ): string {
|
||||
$colors = array(
|
||||
'update' => 'good',
|
||||
'error' => 'danger',
|
||||
'recovery' => 'good',
|
||||
'warning' => 'warning',
|
||||
);
|
||||
/**
|
||||
* 获取类型对应的颜色(Slack 格式)
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return string
|
||||
*/
|
||||
private function get_color_for_type( string $type ): string {
|
||||
$colors = [
|
||||
'update' => 'good',
|
||||
'error' => 'danger',
|
||||
'recovery' => 'good',
|
||||
'warning' => 'warning',
|
||||
];
|
||||
|
||||
return $colors[ $type ] ?? '#0073aa';
|
||||
}
|
||||
return $colors[ $type ] ?? '#0073aa';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型对应的颜色(Discord 整数格式)
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return int
|
||||
*/
|
||||
private function get_color_int_for_type( string $type ): int {
|
||||
$colors = array(
|
||||
'update' => 0x00aa00,
|
||||
'error' => 0xaa0000,
|
||||
'recovery' => 0x00aa00,
|
||||
'warning' => 0xaaaa00,
|
||||
);
|
||||
/**
|
||||
* 获取类型对应的颜色(Discord 整数格式)
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return int
|
||||
*/
|
||||
private function get_color_int_for_type( string $type ): int {
|
||||
$colors = [
|
||||
'update' => 0x00aa00,
|
||||
'error' => 0xaa0000,
|
||||
'recovery' => 0x00aa00,
|
||||
'warning' => 0xaaaa00,
|
||||
];
|
||||
|
||||
return $colors[ $type ] ?? 0x0073aa;
|
||||
}
|
||||
return $colors[ $type ] ?? 0x0073aa;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型对应的颜色(十六进制格式)
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return string
|
||||
*/
|
||||
private function get_color_hex_for_type( string $type ): string {
|
||||
$colors = array(
|
||||
'update' => '00aa00',
|
||||
'error' => 'aa0000',
|
||||
'recovery' => '00aa00',
|
||||
'warning' => 'aaaa00',
|
||||
);
|
||||
/**
|
||||
* 获取类型对应的颜色(十六进制格式)
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return string
|
||||
*/
|
||||
private function get_color_hex_for_type( string $type ): string {
|
||||
$colors = [
|
||||
'update' => '00aa00',
|
||||
'error' => 'aa0000',
|
||||
'recovery' => '00aa00',
|
||||
'warning' => 'aaaa00',
|
||||
];
|
||||
|
||||
return $colors[ $type ] ?? '0073aa';
|
||||
}
|
||||
return $colors[ $type ] ?? '0073aa';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use WPBridge\Cache\CacheManager;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -23,160 +23,157 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class BackgroundUpdater {
|
||||
|
||||
/**
|
||||
* 定时任务钩子名称
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CRON_HOOK = 'wpbridge_update_sources';
|
||||
/**
|
||||
* 定时任务钩子名称
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CRON_HOOK = 'wpbridge_update_sources';
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 源管理器
|
||||
*
|
||||
* @var SourceManager
|
||||
*/
|
||||
private SourceManager $source_manager;
|
||||
/**
|
||||
* 源管理器
|
||||
*
|
||||
* @var SourceManager
|
||||
*/
|
||||
private SourceManager $source_manager;
|
||||
|
||||
/**
|
||||
* 并行请求管理器
|
||||
*
|
||||
* @var ParallelRequestManager
|
||||
*/
|
||||
private ParallelRequestManager $parallel_manager;
|
||||
/**
|
||||
* 并行请求管理器
|
||||
*
|
||||
* @var ParallelRequestManager
|
||||
*/
|
||||
private ParallelRequestManager $parallel_manager;
|
||||
|
||||
/**
|
||||
* 缓存管理器
|
||||
*
|
||||
* @var CacheManager
|
||||
*/
|
||||
private CacheManager $cache;
|
||||
/**
|
||||
* 缓存管理器
|
||||
*
|
||||
* @var CacheManager
|
||||
*/
|
||||
private CacheManager $cache;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->source_manager = new SourceManager( $settings );
|
||||
$this->parallel_manager = new ParallelRequestManager( $settings->get_request_timeout() );
|
||||
$this->cache = new CacheManager();
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->source_manager = new SourceManager( $settings );
|
||||
$this->parallel_manager = new ParallelRequestManager( $settings->get_request_timeout() );
|
||||
$this->cache = new CacheManager();
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
add_action( self::CRON_HOOK, array( $this, 'run_update' ) );
|
||||
}
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
add_action( self::CRON_HOOK, [ $this, 'run_update' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度更新任务
|
||||
*/
|
||||
public function schedule_update(): void {
|
||||
if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
|
||||
wp_schedule_event( time(), 'twicedaily', self::CRON_HOOK );
|
||||
Logger::info( '已调度后台更新任务' );
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 调度更新任务
|
||||
*/
|
||||
public function schedule_update(): void {
|
||||
if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
|
||||
wp_schedule_event( time(), 'twicedaily', self::CRON_HOOK );
|
||||
Logger::info( '已调度后台更新任务' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消调度
|
||||
*/
|
||||
public function unschedule(): void {
|
||||
$timestamp = wp_next_scheduled( self::CRON_HOOK );
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, self::CRON_HOOK );
|
||||
Logger::info( '已取消后台更新任务' );
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 取消调度
|
||||
*/
|
||||
public function unschedule(): void {
|
||||
$timestamp = wp_next_scheduled( self::CRON_HOOK );
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, self::CRON_HOOK );
|
||||
Logger::info( '已取消后台更新任务' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行更新
|
||||
*/
|
||||
public function run_update(): void {
|
||||
Logger::info( '开始后台更新' );
|
||||
/**
|
||||
* 执行更新
|
||||
*/
|
||||
public function run_update(): void {
|
||||
Logger::info( '开始后台更新' );
|
||||
|
||||
$sources = $this->source_manager->get_enabled_sorted();
|
||||
$sources = $this->source_manager->get_enabled_sorted();
|
||||
|
||||
if ( empty( $sources ) ) {
|
||||
Logger::debug( '没有启用的更新源' );
|
||||
return;
|
||||
}
|
||||
if ( empty( $sources ) ) {
|
||||
Logger::debug( '没有启用的更新源' );
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用并行请求检查所有源
|
||||
$results = $this->parallel_manager->check_multiple_sources( $sources );
|
||||
// 使用并行请求检查所有源
|
||||
$results = $this->parallel_manager->check_multiple_sources( $sources );
|
||||
|
||||
$success_count = 0;
|
||||
$fail_count = 0;
|
||||
$success_count = 0;
|
||||
$fail_count = 0;
|
||||
|
||||
foreach ( $results as $source_id => $data ) {
|
||||
if ( null !== $data ) {
|
||||
// 缓存结果
|
||||
$this->cache->set(
|
||||
'source_data_' . $source_id,
|
||||
$data,
|
||||
$this->settings->get_cache_ttl()
|
||||
);
|
||||
++$success_count;
|
||||
} else {
|
||||
++$fail_count;
|
||||
}
|
||||
}
|
||||
foreach ( $results as $source_id => $data ) {
|
||||
if ( null !== $data ) {
|
||||
// 缓存结果
|
||||
$this->cache->set(
|
||||
'source_data_' . $source_id,
|
||||
$data,
|
||||
$this->settings->get_cache_ttl()
|
||||
);
|
||||
$success_count++;
|
||||
} else {
|
||||
$fail_count++;
|
||||
}
|
||||
}
|
||||
|
||||
Logger::info(
|
||||
'后台更新完成',
|
||||
array(
|
||||
'success' => $success_count,
|
||||
'failed' => $fail_count,
|
||||
)
|
||||
);
|
||||
}
|
||||
Logger::info( '后台更新完成', [
|
||||
'success' => $success_count,
|
||||
'failed' => $fail_count,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发更新
|
||||
*
|
||||
* @return array 更新结果
|
||||
*/
|
||||
public function trigger_update(): array {
|
||||
$this->run_update();
|
||||
/**
|
||||
* 手动触发更新
|
||||
*
|
||||
* @return array 更新结果
|
||||
*/
|
||||
public function trigger_update(): array {
|
||||
$this->run_update();
|
||||
|
||||
return array(
|
||||
'status' => 'completed',
|
||||
'time' => current_time( 'mysql' ),
|
||||
);
|
||||
}
|
||||
return [
|
||||
'status' => 'completed',
|
||||
'time' => current_time( 'mysql' ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下次更新时间
|
||||
*
|
||||
* @return int|false 时间戳或 false
|
||||
*/
|
||||
public function get_next_scheduled() {
|
||||
return wp_next_scheduled( self::CRON_HOOK );
|
||||
}
|
||||
/**
|
||||
* 获取下次更新时间
|
||||
*
|
||||
* @return int|false 时间戳或 false
|
||||
*/
|
||||
public function get_next_scheduled() {
|
||||
return wp_next_scheduled( self::CRON_HOOK );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更新状态
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_status(): array {
|
||||
$next = $this->get_next_scheduled();
|
||||
/**
|
||||
* 获取更新状态
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_status(): array {
|
||||
$next = $this->get_next_scheduled();
|
||||
|
||||
return array(
|
||||
'scheduled' => (bool) $next,
|
||||
'next_run' => $next ? gmdate( 'Y-m-d H:i:s', $next ) : null,
|
||||
'next_run_human' => $next ? human_time_diff( time(), $next ) : null,
|
||||
);
|
||||
}
|
||||
return [
|
||||
'scheduled' => (bool) $next,
|
||||
'next_run' => $next ? gmdate( 'Y-m-d H:i:s', $next ) : null,
|
||||
'next_run_human' => $next ? human_time_diff( time(), $next ) : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -21,140 +21,140 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class ConditionalRequest {
|
||||
|
||||
/**
|
||||
* 缓存管理器
|
||||
*
|
||||
* @var CacheManager
|
||||
*/
|
||||
private CacheManager $cache;
|
||||
/**
|
||||
* 缓存管理器
|
||||
*
|
||||
* @var CacheManager
|
||||
*/
|
||||
private CacheManager $cache;
|
||||
|
||||
/**
|
||||
* 缓存前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_PREFIX = 'conditional_';
|
||||
/**
|
||||
* 缓存前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_PREFIX = 'conditional_';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->cache = new CacheManager();
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->cache = new CacheManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建条件请求头
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return array
|
||||
*/
|
||||
public function build_headers( string $source_id ): array {
|
||||
$cached = $this->get_cached_metadata( $source_id );
|
||||
$headers = array();
|
||||
/**
|
||||
* 构建条件请求头
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return array
|
||||
*/
|
||||
public function build_headers( string $source_id ): array {
|
||||
$cached = $this->get_cached_metadata( $source_id );
|
||||
$headers = [];
|
||||
|
||||
if ( ! empty( $cached['etag'] ) ) {
|
||||
$headers['If-None-Match'] = $cached['etag'];
|
||||
}
|
||||
if ( ! empty( $cached['etag'] ) ) {
|
||||
$headers['If-None-Match'] = $cached['etag'];
|
||||
}
|
||||
|
||||
if ( ! empty( $cached['last_modified'] ) ) {
|
||||
$headers['If-Modified-Since'] = $cached['last_modified'];
|
||||
}
|
||||
if ( ! empty( $cached['last_modified'] ) ) {
|
||||
$headers['If-Modified-Since'] = $cached['last_modified'];
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理响应
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param array $response 响应数据
|
||||
* @param array $headers 响应头
|
||||
* @return array|null 处理后的数据,304 时返回缓存数据
|
||||
*/
|
||||
public function process_response( string $source_id, ?array $response, array $headers ): ?array {
|
||||
// 保存元数据
|
||||
$metadata = array();
|
||||
/**
|
||||
* 处理响应
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param array $response 响应数据
|
||||
* @param array $headers 响应头
|
||||
* @return array|null 处理后的数据,304 时返回缓存数据
|
||||
*/
|
||||
public function process_response( string $source_id, ?array $response, array $headers ): ?array {
|
||||
// 保存元数据
|
||||
$metadata = [];
|
||||
|
||||
if ( ! empty( $headers['etag'] ) ) {
|
||||
$metadata['etag'] = $headers['etag'];
|
||||
}
|
||||
if ( ! empty( $headers['etag'] ) ) {
|
||||
$metadata['etag'] = $headers['etag'];
|
||||
}
|
||||
|
||||
if ( ! empty( $headers['last-modified'] ) ) {
|
||||
$metadata['last_modified'] = $headers['last-modified'];
|
||||
}
|
||||
if ( ! empty( $headers['last-modified'] ) ) {
|
||||
$metadata['last_modified'] = $headers['last-modified'];
|
||||
}
|
||||
|
||||
if ( ! empty( $metadata ) ) {
|
||||
$this->save_metadata( $source_id, $metadata );
|
||||
}
|
||||
if ( ! empty( $metadata ) ) {
|
||||
$this->save_metadata( $source_id, $metadata );
|
||||
}
|
||||
|
||||
// 如果有新数据,缓存并返回
|
||||
if ( null !== $response ) {
|
||||
$this->save_cached_data( $source_id, $response );
|
||||
return $response;
|
||||
}
|
||||
// 如果有新数据,缓存并返回
|
||||
if ( null !== $response ) {
|
||||
$this->save_cached_data( $source_id, $response );
|
||||
return $response;
|
||||
}
|
||||
|
||||
// 返回缓存数据
|
||||
return $this->get_cached_data( $source_id );
|
||||
}
|
||||
// 返回缓存数据
|
||||
return $this->get_cached_data( $source_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 304 响应
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return array|null 缓存的数据
|
||||
*/
|
||||
public function handle_not_modified( string $source_id ): ?array {
|
||||
Logger::debug( '304 Not Modified', array( 'source' => $source_id ) );
|
||||
return $this->get_cached_data( $source_id );
|
||||
}
|
||||
/**
|
||||
* 处理 304 响应
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return array|null 缓存的数据
|
||||
*/
|
||||
public function handle_not_modified( string $source_id ): ?array {
|
||||
Logger::debug( '304 Not Modified', [ 'source' => $source_id ] );
|
||||
return $this->get_cached_data( $source_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的元数据
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return array
|
||||
*/
|
||||
private function get_cached_metadata( string $source_id ): array {
|
||||
$cached = $this->cache->get( self::CACHE_PREFIX . 'meta_' . $source_id );
|
||||
return is_array( $cached ) ? $cached : array();
|
||||
}
|
||||
/**
|
||||
* 获取缓存的元数据
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return array
|
||||
*/
|
||||
private function get_cached_metadata( string $source_id ): array {
|
||||
$cached = $this->cache->get( self::CACHE_PREFIX . 'meta_' . $source_id );
|
||||
return is_array( $cached ) ? $cached : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存元数据
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param array $metadata 元数据
|
||||
*/
|
||||
private function save_metadata( string $source_id, array $metadata ): void {
|
||||
$this->cache->set(
|
||||
self::CACHE_PREFIX . 'meta_' . $source_id,
|
||||
$metadata,
|
||||
WEEK_IN_SECONDS
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 保存元数据
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param array $metadata 元数据
|
||||
*/
|
||||
private function save_metadata( string $source_id, array $metadata ): void {
|
||||
$this->cache->set(
|
||||
self::CACHE_PREFIX . 'meta_' . $source_id,
|
||||
$metadata,
|
||||
WEEK_IN_SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的数据
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return array|null
|
||||
*/
|
||||
private function get_cached_data( string $source_id ): ?array {
|
||||
$cached = $this->cache->get( self::CACHE_PREFIX . 'data_' . $source_id );
|
||||
return is_array( $cached ) ? $cached : null;
|
||||
}
|
||||
/**
|
||||
* 获取缓存的数据
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return array|null
|
||||
*/
|
||||
private function get_cached_data( string $source_id ): ?array {
|
||||
$cached = $this->cache->get( self::CACHE_PREFIX . 'data_' . $source_id );
|
||||
return is_array( $cached ) ? $cached : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存缓存数据
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param array $data 数据
|
||||
*/
|
||||
private function save_cached_data( string $source_id, array $data ): void {
|
||||
$this->cache->set(
|
||||
self::CACHE_PREFIX . 'data_' . $source_id,
|
||||
$data,
|
||||
DAY_IN_SECONDS
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 保存缓存数据
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param array $data 数据
|
||||
*/
|
||||
private function save_cached_data( string $source_id, array $data ): void {
|
||||
$this->cache->set(
|
||||
self::CACHE_PREFIX . 'data_' . $source_id,
|
||||
$data,
|
||||
DAY_IN_SECONDS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -21,155 +21,143 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class ParallelRequestManager {
|
||||
|
||||
/**
|
||||
* 默认超时时间(秒)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private int $timeout = 10;
|
||||
/**
|
||||
* 默认超时时间(秒)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private int $timeout = 10;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param int $timeout 超时时间
|
||||
*/
|
||||
public function __construct( int $timeout = 10 ) {
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param int $timeout 超时时间
|
||||
*/
|
||||
public function __construct( int $timeout = 10 ) {
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量检查多个更新源
|
||||
*
|
||||
* @param SourceModel[] $sources 源列表
|
||||
* @return array<string, array|null> 响应数据,键为源 ID
|
||||
*/
|
||||
public function check_multiple_sources( array $sources ): array {
|
||||
if ( empty( $sources ) ) {
|
||||
return array();
|
||||
}
|
||||
/**
|
||||
* 批量检查多个更新源
|
||||
*
|
||||
* @param SourceModel[] $sources 源列表
|
||||
* @return array<string, array|null> 响应数据,键为源 ID
|
||||
*/
|
||||
public function check_multiple_sources( array $sources ): array {
|
||||
if ( empty( $sources ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$requests = array();
|
||||
$requests = [];
|
||||
|
||||
foreach ( $sources as $source ) {
|
||||
$requests[ $source->id ] = array(
|
||||
'url' => $source->get_check_url(),
|
||||
'type' => \WpOrg\Requests\Requests::GET,
|
||||
'headers' => $source->get_headers(),
|
||||
);
|
||||
}
|
||||
foreach ( $sources as $source ) {
|
||||
$requests[ $source->id ] = [
|
||||
'url' => $source->get_check_url(),
|
||||
'type' => \WpOrg\Requests\Requests::GET,
|
||||
'headers' => $source->get_headers(),
|
||||
];
|
||||
}
|
||||
|
||||
Logger::debug( '开始并行请求', array( 'count' => count( $requests ) ) );
|
||||
Logger::debug( '开始并行请求', [ 'count' => count( $requests ) ] );
|
||||
|
||||
$start = microtime( true );
|
||||
$start = microtime( true );
|
||||
|
||||
// 使用 WordPress Requests API 并行请求
|
||||
$responses = \WpOrg\Requests\Requests::request_multiple(
|
||||
$requests,
|
||||
array(
|
||||
'timeout' => $this->timeout,
|
||||
'connect_timeout' => 5,
|
||||
'follow_redirects' => true,
|
||||
'redirects' => 3,
|
||||
)
|
||||
);
|
||||
// 使用 WordPress Requests API 并行请求
|
||||
$responses = \WpOrg\Requests\Requests::request_multiple(
|
||||
$requests,
|
||||
[
|
||||
'timeout' => $this->timeout,
|
||||
'connect_timeout' => 5,
|
||||
'follow_redirects' => true,
|
||||
'redirects' => 3,
|
||||
]
|
||||
);
|
||||
|
||||
$elapsed = round( ( microtime( true ) - $start ) * 1000 );
|
||||
$elapsed = round( ( microtime( true ) - $start ) * 1000 );
|
||||
|
||||
Logger::debug(
|
||||
'并行请求完成',
|
||||
array(
|
||||
'count' => count( $requests ),
|
||||
'time_ms' => $elapsed,
|
||||
)
|
||||
);
|
||||
Logger::debug( '并行请求完成', [
|
||||
'count' => count( $requests ),
|
||||
'time_ms' => $elapsed,
|
||||
] );
|
||||
|
||||
return $this->process_responses( $responses );
|
||||
}
|
||||
return $this->process_responses( $responses );
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理响应
|
||||
*
|
||||
* @param array $responses 响应数组
|
||||
* @return array<string, array|null>
|
||||
*/
|
||||
private function process_responses( array $responses ): array {
|
||||
$results = array();
|
||||
/**
|
||||
* 处理响应
|
||||
*
|
||||
* @param array $responses 响应数组
|
||||
* @return array<string, array|null>
|
||||
*/
|
||||
private function process_responses( array $responses ): array {
|
||||
$results = [];
|
||||
|
||||
foreach ( $responses as $source_id => $response ) {
|
||||
if ( $response instanceof \WpOrg\Requests\Exception ) {
|
||||
Logger::warning(
|
||||
'请求失败',
|
||||
array(
|
||||
'source' => $source_id,
|
||||
'error' => $response->getMessage(),
|
||||
)
|
||||
);
|
||||
$results[ $source_id ] = null;
|
||||
continue;
|
||||
}
|
||||
foreach ( $responses as $source_id => $response ) {
|
||||
if ( $response instanceof \WpOrg\Requests\Exception ) {
|
||||
Logger::warning( '请求失败', [
|
||||
'source' => $source_id,
|
||||
'error' => $response->getMessage(),
|
||||
] );
|
||||
$results[ $source_id ] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! $response->success ) {
|
||||
Logger::warning(
|
||||
'请求返回非成功状态',
|
||||
array(
|
||||
'source' => $source_id,
|
||||
'status' => $response->status_code,
|
||||
)
|
||||
);
|
||||
$results[ $source_id ] = null;
|
||||
continue;
|
||||
}
|
||||
if ( ! $response->success ) {
|
||||
Logger::warning( '请求返回非成功状态', [
|
||||
'source' => $source_id,
|
||||
'status' => $response->status_code,
|
||||
] );
|
||||
$results[ $source_id ] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = json_decode( $response->body, true );
|
||||
$data = json_decode( $response->body, true );
|
||||
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
Logger::warning(
|
||||
'JSON 解析失败',
|
||||
array(
|
||||
'source' => $source_id,
|
||||
'error' => json_last_error_msg(),
|
||||
)
|
||||
);
|
||||
$results[ $source_id ] = null;
|
||||
continue;
|
||||
}
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
Logger::warning( 'JSON 解析失败', [
|
||||
'source' => $source_id,
|
||||
'error' => json_last_error_msg(),
|
||||
] );
|
||||
$results[ $source_id ] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
$results[ $source_id ] = $data;
|
||||
}
|
||||
$results[ $source_id ] = $data;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量请求 URL
|
||||
*
|
||||
* @param array $urls URL 数组,键为标识符
|
||||
* @param array $headers 公共请求头
|
||||
* @return array<string, array|null>
|
||||
*/
|
||||
public function fetch_multiple( array $urls, array $headers = array() ): array {
|
||||
if ( empty( $urls ) ) {
|
||||
return array();
|
||||
}
|
||||
/**
|
||||
* 批量请求 URL
|
||||
*
|
||||
* @param array $urls URL 数组,键为标识符
|
||||
* @param array $headers 公共请求头
|
||||
* @return array<string, array|null>
|
||||
*/
|
||||
public function fetch_multiple( array $urls, array $headers = [] ): array {
|
||||
if ( empty( $urls ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$requests = array();
|
||||
$requests = [];
|
||||
|
||||
foreach ( $urls as $key => $url ) {
|
||||
$requests[ $key ] = array(
|
||||
'url' => $url,
|
||||
'type' => \WpOrg\Requests\Requests::GET,
|
||||
'headers' => $headers,
|
||||
);
|
||||
}
|
||||
foreach ( $urls as $key => $url ) {
|
||||
$requests[ $key ] = [
|
||||
'url' => $url,
|
||||
'type' => \WpOrg\Requests\Requests::GET,
|
||||
'headers' => $headers,
|
||||
];
|
||||
}
|
||||
|
||||
$responses = \WpOrg\Requests\Requests::request_multiple(
|
||||
$requests,
|
||||
array(
|
||||
'timeout' => $this->timeout,
|
||||
'connect_timeout' => 5,
|
||||
)
|
||||
);
|
||||
$responses = \WpOrg\Requests\Requests::request_multiple(
|
||||
$requests,
|
||||
[
|
||||
'timeout' => $this->timeout,
|
||||
'connect_timeout' => 5,
|
||||
]
|
||||
);
|
||||
|
||||
return $this->process_responses( $responses );
|
||||
}
|
||||
return $this->process_responses( $responses );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20,99 +20,99 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class RequestDeduplicator {
|
||||
|
||||
/**
|
||||
* 合并窗口时间(秒)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MERGE_WINDOW = 5;
|
||||
/**
|
||||
* 合并窗口时间(秒)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MERGE_WINDOW = 5;
|
||||
|
||||
/**
|
||||
* 锁前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const LOCK_PREFIX = 'wpbridge_lock_';
|
||||
/**
|
||||
* 锁前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const LOCK_PREFIX = 'wpbridge_lock_';
|
||||
|
||||
/**
|
||||
* 尝试获取锁
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool 是否成功获取锁
|
||||
*/
|
||||
public function acquire_lock( string $source_id ): bool {
|
||||
$lock_key = self::LOCK_PREFIX . $source_id;
|
||||
/**
|
||||
* 尝试获取锁
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool 是否成功获取锁
|
||||
*/
|
||||
public function acquire_lock( string $source_id ): bool {
|
||||
$lock_key = self::LOCK_PREFIX . $source_id;
|
||||
|
||||
// 检查是否已有锁
|
||||
if ( get_transient( $lock_key ) ) {
|
||||
Logger::debug( '请求被去重', array( 'source' => $source_id ) );
|
||||
return false;
|
||||
}
|
||||
// 检查是否已有锁
|
||||
if ( get_transient( $lock_key ) ) {
|
||||
Logger::debug( '请求被去重', [ 'source' => $source_id ] );
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置锁
|
||||
set_transient( $lock_key, time(), self::MERGE_WINDOW );
|
||||
return true;
|
||||
}
|
||||
// 设置锁
|
||||
set_transient( $lock_key, time(), self::MERGE_WINDOW );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
public function release_lock( string $source_id ): void {
|
||||
delete_transient( self::LOCK_PREFIX . $source_id );
|
||||
}
|
||||
/**
|
||||
* 释放锁
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
public function release_lock( string $source_id ): void {
|
||||
delete_transient( self::LOCK_PREFIX . $source_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有锁
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function has_lock( string $source_id ): bool {
|
||||
return (bool) get_transient( self::LOCK_PREFIX . $source_id );
|
||||
}
|
||||
/**
|
||||
* 检查是否有锁
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function has_lock( string $source_id ): bool {
|
||||
return (bool) get_transient( self::LOCK_PREFIX . $source_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待锁释放并获取结果
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param callable $callback 获取结果的回调
|
||||
* @param int $max_wait 最大等待时间(秒)
|
||||
* @return mixed
|
||||
*/
|
||||
public function wait_and_get( string $source_id, callable $callback, int $max_wait = 10 ) {
|
||||
$start = time();
|
||||
/**
|
||||
* 等待锁释放并获取结果
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param callable $callback 获取结果的回调
|
||||
* @param int $max_wait 最大等待时间(秒)
|
||||
* @return mixed
|
||||
*/
|
||||
public function wait_and_get( string $source_id, callable $callback, int $max_wait = 10 ) {
|
||||
$start = time();
|
||||
|
||||
while ( $this->has_lock( $source_id ) ) {
|
||||
if ( ( time() - $start ) >= $max_wait ) {
|
||||
Logger::warning( '等待锁超时', array( 'source' => $source_id ) );
|
||||
break;
|
||||
}
|
||||
usleep( 100000 ); // 100ms
|
||||
}
|
||||
while ( $this->has_lock( $source_id ) ) {
|
||||
if ( ( time() - $start ) >= $max_wait ) {
|
||||
Logger::warning( '等待锁超时', [ 'source' => $source_id ] );
|
||||
break;
|
||||
}
|
||||
usleep( 100000 ); // 100ms
|
||||
}
|
||||
|
||||
return $callback();
|
||||
}
|
||||
return $callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* 带锁执行操作
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param callable $callback 操作回调
|
||||
* @return mixed
|
||||
*/
|
||||
public function execute_with_lock( string $source_id, callable $callback ) {
|
||||
if ( ! $this->acquire_lock( $source_id ) ) {
|
||||
// 已有请求在进行中,等待结果
|
||||
return $this->wait_and_get( $source_id, $callback );
|
||||
}
|
||||
/**
|
||||
* 带锁执行操作
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @param callable $callback 操作回调
|
||||
* @return mixed
|
||||
*/
|
||||
public function execute_with_lock( string $source_id, callable $callback ) {
|
||||
if ( ! $this->acquire_lock( $source_id ) ) {
|
||||
// 已有请求在进行中,等待结果
|
||||
return $this->wait_and_get( $source_id, $callback );
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $callback();
|
||||
return $result;
|
||||
} finally {
|
||||
$this->release_lock( $source_id );
|
||||
}
|
||||
}
|
||||
try {
|
||||
$result = $callback();
|
||||
return $result;
|
||||
} finally {
|
||||
$this->release_lock( $source_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\Security;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,196 +17,196 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class Encryption {
|
||||
|
||||
/**
|
||||
* 加密方法
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const METHOD = 'aes-256-cbc';
|
||||
/**
|
||||
* 加密方法
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const METHOD = 'aes-256-cbc';
|
||||
|
||||
/**
|
||||
* 获取加密密钥
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_key(): string {
|
||||
// 优先使用自定义密钥
|
||||
if ( defined( 'WPBRIDGE_ENCRYPTION_KEY' ) && WPBRIDGE_ENCRYPTION_KEY ) {
|
||||
return WPBRIDGE_ENCRYPTION_KEY;
|
||||
}
|
||||
/**
|
||||
* 获取加密密钥
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_key(): string {
|
||||
// 优先使用自定义密钥
|
||||
if ( defined( 'WPBRIDGE_ENCRYPTION_KEY' ) && WPBRIDGE_ENCRYPTION_KEY ) {
|
||||
return WPBRIDGE_ENCRYPTION_KEY;
|
||||
}
|
||||
|
||||
// 使用 WordPress 的 AUTH_KEY
|
||||
if ( defined( 'AUTH_KEY' ) && AUTH_KEY ) {
|
||||
return AUTH_KEY;
|
||||
}
|
||||
// 使用 WordPress 的 AUTH_KEY
|
||||
if ( defined( 'AUTH_KEY' ) && AUTH_KEY ) {
|
||||
return AUTH_KEY;
|
||||
}
|
||||
|
||||
// 最后使用 SECURE_AUTH_KEY
|
||||
if ( defined( 'SECURE_AUTH_KEY' ) && SECURE_AUTH_KEY ) {
|
||||
return SECURE_AUTH_KEY;
|
||||
}
|
||||
// 最后使用 SECURE_AUTH_KEY
|
||||
if ( defined( 'SECURE_AUTH_KEY' ) && SECURE_AUTH_KEY ) {
|
||||
return SECURE_AUTH_KEY;
|
||||
}
|
||||
|
||||
// 如果都没有,生成并存储一个随机密钥
|
||||
$key = get_option( 'wpbridge_encryption_key' );
|
||||
if ( empty( $key ) ) {
|
||||
$key = bin2hex( random_bytes( 32 ) );
|
||||
update_option( 'wpbridge_encryption_key', $key, false );
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
// 如果都没有,生成并存储一个随机密钥
|
||||
$key = get_option( 'wpbridge_encryption_key' );
|
||||
if ( empty( $key ) ) {
|
||||
$key = bin2hex( random_bytes( 32 ) );
|
||||
update_option( 'wpbridge_encryption_key', $key, false );
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密数据
|
||||
*
|
||||
* @param string $data 明文数据
|
||||
* @return string 加密后的数据(base64 编码)
|
||||
*/
|
||||
public static function encrypt( string $data ): string {
|
||||
if ( empty( $data ) ) {
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* 加密数据
|
||||
*
|
||||
* @param string $data 明文数据
|
||||
* @return string 加密后的数据(base64 编码)
|
||||
*/
|
||||
public static function encrypt( string $data ): string {
|
||||
if ( empty( $data ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$key = hash( 'sha256', self::get_key(), true );
|
||||
$iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::METHOD ) );
|
||||
$key = hash( 'sha256', self::get_key(), true );
|
||||
$iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::METHOD ) );
|
||||
|
||||
$encrypted = openssl_encrypt( $data, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
|
||||
$encrypted = openssl_encrypt( $data, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
|
||||
|
||||
if ( false === $encrypted ) {
|
||||
return '';
|
||||
}
|
||||
if ( false === $encrypted ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 将 IV 和加密数据一起存储
|
||||
return base64_encode( $iv . $encrypted );
|
||||
}
|
||||
// 将 IV 和加密数据一起存储
|
||||
return base64_encode( $iv . $encrypted );
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密数据
|
||||
*
|
||||
* @param string $data 加密数据(base64 编码)
|
||||
* @return string 解密后的明文
|
||||
*/
|
||||
public static function decrypt( string $data ): string {
|
||||
if ( empty( $data ) ) {
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* 解密数据
|
||||
*
|
||||
* @param string $data 加密数据(base64 编码)
|
||||
* @return string 解密后的明文
|
||||
*/
|
||||
public static function decrypt( string $data ): string {
|
||||
if ( empty( $data ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = base64_decode( $data );
|
||||
$data = base64_decode( $data );
|
||||
|
||||
if ( false === $data ) {
|
||||
return '';
|
||||
}
|
||||
if ( false === $data ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$key = hash( 'sha256', self::get_key(), true );
|
||||
$iv_length = openssl_cipher_iv_length( self::METHOD );
|
||||
$key = hash( 'sha256', self::get_key(), true );
|
||||
$iv_length = openssl_cipher_iv_length( self::METHOD );
|
||||
|
||||
if ( strlen( $data ) < $iv_length ) {
|
||||
return '';
|
||||
}
|
||||
if ( strlen( $data ) < $iv_length ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$iv = substr( $data, 0, $iv_length );
|
||||
$encrypted = substr( $data, $iv_length );
|
||||
$iv = substr( $data, 0, $iv_length );
|
||||
$encrypted = substr( $data, $iv_length );
|
||||
|
||||
$decrypted = openssl_decrypt( $encrypted, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
|
||||
$decrypted = openssl_decrypt( $encrypted, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
|
||||
|
||||
if ( false === $decrypted ) {
|
||||
return '';
|
||||
}
|
||||
if ( false === $decrypted ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $decrypted;
|
||||
}
|
||||
return $decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据是否已加密
|
||||
*
|
||||
* @param string $data 数据
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_encrypted( string $data ): bool {
|
||||
if ( empty( $data ) ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 检查数据是否已加密
|
||||
*
|
||||
* @param string $data 数据
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_encrypted( string $data ): bool {
|
||||
if ( empty( $data ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是有效的 base64
|
||||
$decoded = base64_decode( $data, true );
|
||||
// 检查是否是有效的 base64
|
||||
$decoded = base64_decode( $data, true );
|
||||
|
||||
if ( false === $decoded ) {
|
||||
return false;
|
||||
}
|
||||
if ( false === $decoded ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查长度是否足够包含 IV
|
||||
$iv_length = openssl_cipher_iv_length( self::METHOD );
|
||||
// 检查长度是否足够包含 IV
|
||||
$iv_length = openssl_cipher_iv_length( self::METHOD );
|
||||
|
||||
return strlen( $decoded ) > $iv_length;
|
||||
}
|
||||
return strlen( $decoded ) > $iv_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地存储敏感数据
|
||||
*
|
||||
* @param string $key 选项键
|
||||
* @param string $value 敏感值
|
||||
* @return bool
|
||||
*/
|
||||
public static function store_secure( string $key, string $value ): bool {
|
||||
$encrypted = self::encrypt( $value );
|
||||
return update_option( 'wpbridge_secure_' . $key, $encrypted );
|
||||
}
|
||||
/**
|
||||
* 安全地存储敏感数据
|
||||
*
|
||||
* @param string $key 选项键
|
||||
* @param string $value 敏感值
|
||||
* @return bool
|
||||
*/
|
||||
public static function store_secure( string $key, string $value ): bool {
|
||||
$encrypted = self::encrypt( $value );
|
||||
return update_option( 'wpbridge_secure_' . $key, $encrypted );
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地获取敏感数据
|
||||
*
|
||||
* @param string $key 选项键
|
||||
* @param string $default 默认值
|
||||
* @return string
|
||||
*/
|
||||
public static function get_secure( string $key, string $default = '' ): string {
|
||||
$encrypted = get_option( 'wpbridge_secure_' . $key, '' );
|
||||
/**
|
||||
* 安全地获取敏感数据
|
||||
*
|
||||
* @param string $key 选项键
|
||||
* @param string $default 默认值
|
||||
* @return string
|
||||
*/
|
||||
public static function get_secure( string $key, string $default = '' ): string {
|
||||
$encrypted = get_option( 'wpbridge_secure_' . $key, '' );
|
||||
|
||||
if ( empty( $encrypted ) ) {
|
||||
return $default;
|
||||
}
|
||||
if ( empty( $encrypted ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$decrypted = self::decrypt( $encrypted );
|
||||
$decrypted = self::decrypt( $encrypted );
|
||||
|
||||
return ! empty( $decrypted ) ? $decrypted : $default;
|
||||
}
|
||||
return ! empty( $decrypted ) ? $decrypted : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除安全存储的数据
|
||||
*
|
||||
* @param string $key 选项键
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete_secure( string $key ): bool {
|
||||
return delete_option( 'wpbridge_secure_' . $key );
|
||||
}
|
||||
/**
|
||||
* 删除安全存储的数据
|
||||
*
|
||||
* @param string $key 选项键
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete_secure( string $key ): bool {
|
||||
return delete_option( 'wpbridge_secure_' . $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机令牌
|
||||
*
|
||||
* @param int $length 长度
|
||||
* @return string
|
||||
*/
|
||||
public static function generate_token( int $length = 32 ): string {
|
||||
return bin2hex( random_bytes( $length / 2 ) );
|
||||
}
|
||||
/**
|
||||
* 生成随机令牌
|
||||
*
|
||||
* @param int $length 长度
|
||||
* @return string
|
||||
*/
|
||||
public static function generate_token( int $length = 32 ): string {
|
||||
return bin2hex( random_bytes( $length / 2 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 哈希密码/令牌(用于比较)
|
||||
*
|
||||
* @param string $data 数据
|
||||
* @return string
|
||||
*/
|
||||
public static function hash( string $data ): string {
|
||||
return hash( 'sha256', $data . self::get_key() );
|
||||
}
|
||||
/**
|
||||
* 哈希密码/令牌(用于比较)
|
||||
*
|
||||
* @param string $data 数据
|
||||
* @return string
|
||||
*/
|
||||
public static function hash( string $data ): string {
|
||||
return hash( 'sha256', $data . self::get_key() );
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证哈希
|
||||
*
|
||||
* @param string $data 原始数据
|
||||
* @param string $hash 哈希值
|
||||
* @return bool
|
||||
*/
|
||||
public static function verify_hash( string $data, string $hash ): bool {
|
||||
return hash_equals( self::hash( $data ), $hash );
|
||||
}
|
||||
/**
|
||||
* 验证哈希
|
||||
*
|
||||
* @param string $data 原始数据
|
||||
* @param string $hash 哈希值
|
||||
* @return bool
|
||||
*/
|
||||
public static function verify_hash( string $data, string $hash ): bool {
|
||||
return hash_equals( self::hash( $data ), $hash );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\Security;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,213 +17,213 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class Validator {
|
||||
|
||||
/**
|
||||
* 校验 URL
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid_url( string $url ): bool {
|
||||
if ( empty( $url ) ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 校验 URL
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid_url( string $url ): bool {
|
||||
if ( empty( $url ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 基本格式校验
|
||||
if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
|
||||
return false;
|
||||
}
|
||||
// 基本格式校验
|
||||
if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只允许 http 和 https
|
||||
$scheme = parse_url( $url, PHP_URL_SCHEME );
|
||||
if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
// 只允许 http 和 https
|
||||
$scheme = parse_url( $url, PHP_URL_SCHEME );
|
||||
if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有主机名
|
||||
$host = parse_url( $url, PHP_URL_HOST );
|
||||
if ( empty( $host ) ) {
|
||||
return false;
|
||||
}
|
||||
// 检查是否有主机名
|
||||
$host = parse_url( $url, PHP_URL_HOST );
|
||||
if ( empty( $host ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 禁止本地地址(安全考虑)
|
||||
if ( self::is_local_address( $host ) ) {
|
||||
return false;
|
||||
}
|
||||
// 禁止本地地址(安全考虑)
|
||||
if ( self::is_local_address( $host ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是本地地址
|
||||
*
|
||||
* @param string $host 主机名
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_local_address( string $host ): bool {
|
||||
// 本地主机名
|
||||
$local_hosts = array( 'localhost', '127.0.0.1', '::1' );
|
||||
/**
|
||||
* 检查是否是本地地址
|
||||
*
|
||||
* @param string $host 主机名
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_local_address( string $host ): bool {
|
||||
// 本地主机名
|
||||
$local_hosts = [ 'localhost', '127.0.0.1', '::1' ];
|
||||
|
||||
if ( in_array( $host, $local_hosts, true ) ) {
|
||||
return true;
|
||||
}
|
||||
if ( in_array( $host, $local_hosts, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 私有 IP 范围
|
||||
$ip = gethostbyname( $host );
|
||||
// 私有 IP 范围
|
||||
$ip = gethostbyname( $host );
|
||||
|
||||
if ( $ip === $host ) {
|
||||
// 无法解析,为安全起见视为本地地址
|
||||
return true;
|
||||
}
|
||||
if ( $ip === $host ) {
|
||||
// 无法解析,为安全起见视为本地地址
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查私有 IP 范围(IPv4)
|
||||
$private_ranges = array(
|
||||
'10.0.0.0/8',
|
||||
'172.16.0.0/12',
|
||||
'192.168.0.0/16',
|
||||
'127.0.0.0/8',
|
||||
);
|
||||
// 检查私有 IP 范围(IPv4)
|
||||
$private_ranges = [
|
||||
'10.0.0.0/8',
|
||||
'172.16.0.0/12',
|
||||
'192.168.0.0/16',
|
||||
'127.0.0.0/8',
|
||||
];
|
||||
|
||||
foreach ( $private_ranges as $range ) {
|
||||
if ( self::ip_in_range( $ip, $range ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
foreach ( $private_ranges as $range ) {
|
||||
if ( self::ip_in_range( $ip, $range ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 IPv6 私有地址
|
||||
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
|
||||
// fc00::/7 (Unique Local Addresses)
|
||||
// fe80::/10 (Link-Local Addresses)
|
||||
if ( preg_match( '/^(fc|fd|fe80)/i', $ip ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 检查 IPv6 私有地址
|
||||
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
|
||||
// fc00::/7 (Unique Local Addresses)
|
||||
// fe80::/10 (Link-Local Addresses)
|
||||
if ( preg_match( '/^(fc|fd|fe80)/i', $ip ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 IP 是否在范围内
|
||||
*
|
||||
* @param string $ip IP 地址
|
||||
* @param string $range CIDR 范围
|
||||
* @return bool
|
||||
*/
|
||||
private static function ip_in_range( string $ip, string $range ): bool {
|
||||
list( $subnet, $bits ) = explode( '/', $range );
|
||||
/**
|
||||
* 检查 IP 是否在范围内
|
||||
*
|
||||
* @param string $ip IP 地址
|
||||
* @param string $range CIDR 范围
|
||||
* @return bool
|
||||
*/
|
||||
private static function ip_in_range( string $ip, string $range ): bool {
|
||||
list( $subnet, $bits ) = explode( '/', $range );
|
||||
|
||||
$ip_long = ip2long( $ip );
|
||||
$subnet_long = ip2long( $subnet );
|
||||
$mask = -1 << ( 32 - (int) $bits );
|
||||
$ip_long = ip2long( $ip );
|
||||
$subnet_long = ip2long( $subnet );
|
||||
$mask = -1 << ( 32 - (int) $bits );
|
||||
|
||||
return ( $ip_long & $mask ) === ( $subnet_long & $mask );
|
||||
}
|
||||
return ( $ip_long & $mask ) === ( $subnet_long & $mask );
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验版本号
|
||||
*
|
||||
* @param string $version 版本号
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid_version( string $version ): bool {
|
||||
if ( empty( $version ) ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 校验版本号
|
||||
*
|
||||
* @param string $version 版本号
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid_version( string $version ): bool {
|
||||
if ( empty( $version ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 支持语义化版本和 WordPress 风格版本
|
||||
// 例如: 1.0.0, 1.0, 1.0.0-beta, 1.0.0-rc.1
|
||||
$pattern = '/^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$/';
|
||||
// 支持语义化版本和 WordPress 风格版本
|
||||
// 例如: 1.0.0, 1.0, 1.0.0-beta, 1.0.0-rc.1
|
||||
$pattern = '/^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$/';
|
||||
|
||||
return (bool) preg_match( $pattern, $version );
|
||||
}
|
||||
return (bool) preg_match( $pattern, $version );
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 slug
|
||||
*
|
||||
* @param string $slug Slug
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid_slug( string $slug ): bool {
|
||||
if ( empty( $slug ) ) {
|
||||
return true; // 空 slug 是允许的(表示匹配所有)
|
||||
}
|
||||
/**
|
||||
* 校验 slug
|
||||
*
|
||||
* @param string $slug Slug
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid_slug( string $slug ): bool {
|
||||
if ( empty( $slug ) ) {
|
||||
return true; // 空 slug 是允许的(表示匹配所有)
|
||||
}
|
||||
|
||||
// 只允许小写字母、数字、连字符
|
||||
$pattern = '/^[a-z0-9-]+$/';
|
||||
// 只允许小写字母、数字、连字符
|
||||
$pattern = '/^[a-z0-9-]+$/';
|
||||
|
||||
return (bool) preg_match( $pattern, $slug );
|
||||
}
|
||||
return (bool) preg_match( $pattern, $slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 JSON 响应结构
|
||||
*
|
||||
* @param array $data 数据
|
||||
* @param array $required 必需字段
|
||||
* @return array 错误数组
|
||||
*/
|
||||
public static function validate_json_structure( array $data, array $required ): array {
|
||||
$errors = array();
|
||||
/**
|
||||
* 校验 JSON 响应结构
|
||||
*
|
||||
* @param array $data 数据
|
||||
* @param array $required 必需字段
|
||||
* @return array 错误数组
|
||||
*/
|
||||
public static function validate_json_structure( array $data, array $required ): array {
|
||||
$errors = [];
|
||||
|
||||
foreach ( $required as $field ) {
|
||||
if ( ! isset( $data[ $field ] ) ) {
|
||||
$errors[] = sprintf( __( '缺少必需字段: %s', 'wpbridge' ), $field );
|
||||
}
|
||||
}
|
||||
foreach ( $required as $field ) {
|
||||
if ( ! isset( $data[ $field ] ) ) {
|
||||
$errors[] = sprintf( __( '缺少必需字段: %s', 'wpbridge' ), $field );
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验更新信息 JSON
|
||||
*
|
||||
* @param array $data 数据
|
||||
* @return array 错误数组
|
||||
*/
|
||||
public static function validate_update_info( array $data ): array {
|
||||
$errors = array();
|
||||
/**
|
||||
* 校验更新信息 JSON
|
||||
*
|
||||
* @param array $data 数据
|
||||
* @return array 错误数组
|
||||
*/
|
||||
public static function validate_update_info( array $data ): array {
|
||||
$errors = [];
|
||||
|
||||
// 必需字段
|
||||
if ( empty( $data['version'] ) ) {
|
||||
$errors[] = __( '缺少版本号', 'wpbridge' );
|
||||
} elseif ( ! self::is_valid_version( $data['version'] ) ) {
|
||||
$errors[] = __( '无效的版本号格式', 'wpbridge' );
|
||||
}
|
||||
// 必需字段
|
||||
if ( empty( $data['version'] ) ) {
|
||||
$errors[] = __( '缺少版本号', 'wpbridge' );
|
||||
} elseif ( ! self::is_valid_version( $data['version'] ) ) {
|
||||
$errors[] = __( '无效的版本号格式', 'wpbridge' );
|
||||
}
|
||||
|
||||
// 下载 URL
|
||||
$download_url = $data['download_url'] ?? $data['package'] ?? '';
|
||||
if ( ! empty( $download_url ) && ! self::is_valid_url( $download_url ) ) {
|
||||
$errors[] = __( '无效的下载 URL', 'wpbridge' );
|
||||
}
|
||||
// 下载 URL
|
||||
$download_url = $data['download_url'] ?? $data['package'] ?? '';
|
||||
if ( ! empty( $download_url ) && ! self::is_valid_url( $download_url ) ) {
|
||||
$errors[] = __( '无效的下载 URL', 'wpbridge' );
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理 HTML
|
||||
*
|
||||
* @param string $html HTML 内容
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_html( string $html ): string {
|
||||
return wp_kses_post( $html );
|
||||
}
|
||||
/**
|
||||
* 清理 HTML
|
||||
*
|
||||
* @param string $html HTML 内容
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_html( string $html ): string {
|
||||
return wp_kses_post( $html );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理文本
|
||||
*
|
||||
* @param string $text 文本
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_text( string $text ): string {
|
||||
return sanitize_text_field( $text );
|
||||
}
|
||||
/**
|
||||
* 清理文本
|
||||
*
|
||||
* @param string $text 文本
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_text( string $text ): string {
|
||||
return sanitize_text_field( $text );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理 URL
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_url( string $url ): string {
|
||||
return esc_url_raw( $url );
|
||||
}
|
||||
/**
|
||||
* 清理 URL
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_url( string $url ): string {
|
||||
return esc_url_raw( $url );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use WPBridge\UpdateSource\SourceManager;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,342 +22,333 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class GroupManager {
|
||||
|
||||
/**
|
||||
* 选项名称
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_source_groups';
|
||||
/**
|
||||
* 选项名称
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const OPTION_NAME = 'wpbridge_source_groups';
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 源管理器
|
||||
*
|
||||
* @var SourceManager
|
||||
*/
|
||||
private SourceManager $source_manager;
|
||||
/**
|
||||
* 源管理器
|
||||
*
|
||||
* @var SourceManager
|
||||
*/
|
||||
private SourceManager $source_manager;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->source_manager = new SourceManager( $settings );
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->source_manager = new SourceManager( $settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有分组
|
||||
*
|
||||
* @return GroupModel[]
|
||||
*/
|
||||
public function get_all(): array {
|
||||
$groups_data = get_option( self::OPTION_NAME, array() );
|
||||
$groups = array();
|
||||
/**
|
||||
* 获取所有分组
|
||||
*
|
||||
* @return GroupModel[]
|
||||
*/
|
||||
public function get_all(): array {
|
||||
$groups_data = get_option( self::OPTION_NAME, [] );
|
||||
$groups = [];
|
||||
|
||||
foreach ( $groups_data as $data ) {
|
||||
$groups[] = GroupModel::from_array( $data );
|
||||
}
|
||||
foreach ( $groups_data as $data ) {
|
||||
$groups[] = GroupModel::from_array( $data );
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个分组
|
||||
*
|
||||
* @param string $id 分组 ID
|
||||
* @return GroupModel|null
|
||||
*/
|
||||
public function get( string $id ): ?GroupModel {
|
||||
$groups = $this->get_all();
|
||||
/**
|
||||
* 获取单个分组
|
||||
*
|
||||
* @param string $id 分组 ID
|
||||
* @return GroupModel|null
|
||||
*/
|
||||
public function get( string $id ): ?GroupModel {
|
||||
$groups = $this->get_all();
|
||||
|
||||
foreach ( $groups as $group ) {
|
||||
if ( $group->id === $id ) {
|
||||
return $group;
|
||||
}
|
||||
}
|
||||
foreach ( $groups as $group ) {
|
||||
if ( $group->id === $id ) {
|
||||
return $group;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加分组
|
||||
*
|
||||
* @param GroupModel $group 分组模型
|
||||
* @return bool
|
||||
*/
|
||||
public function add( GroupModel $group ): bool {
|
||||
if ( ! $group->is_valid() ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 添加分组
|
||||
*
|
||||
* @param GroupModel $group 分组模型
|
||||
* @return bool
|
||||
*/
|
||||
public function add( GroupModel $group ): bool {
|
||||
if ( ! $group->is_valid() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = $this->get_all();
|
||||
$groups = $this->get_all();
|
||||
|
||||
// 生成 ID
|
||||
if ( empty( $group->id ) ) {
|
||||
$group->id = 'group_' . wp_generate_uuid4();
|
||||
}
|
||||
// 生成 ID
|
||||
if ( empty( $group->id ) ) {
|
||||
$group->id = 'group_' . wp_generate_uuid4();
|
||||
}
|
||||
|
||||
$group->created_at = current_time( 'mysql' );
|
||||
$group->updated_at = $group->created_at;
|
||||
$group->created_at = current_time( 'mysql' );
|
||||
$group->updated_at = $group->created_at;
|
||||
|
||||
// 加密共享认证令牌
|
||||
if ( ! empty( $group->shared_auth_token ) ) {
|
||||
$group->shared_auth_token = Encryption::encrypt( $group->shared_auth_token );
|
||||
}
|
||||
// 加密共享认证令牌
|
||||
if ( ! empty( $group->shared_auth_token ) ) {
|
||||
$group->shared_auth_token = Encryption::encrypt( $group->shared_auth_token );
|
||||
}
|
||||
|
||||
$groups[] = $group;
|
||||
$groups[] = $group;
|
||||
|
||||
Logger::info(
|
||||
'添加源分组',
|
||||
array(
|
||||
'id' => $group->id,
|
||||
'name' => $group->name,
|
||||
)
|
||||
);
|
||||
Logger::info( '添加源分组', [ 'id' => $group->id, 'name' => $group->name ] );
|
||||
|
||||
return $this->save_groups( $groups );
|
||||
}
|
||||
return $this->save_groups( $groups );
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分组
|
||||
*
|
||||
* @param GroupModel $group 分组模型
|
||||
* @return bool
|
||||
*/
|
||||
public function update( GroupModel $group ): bool {
|
||||
if ( ! $group->is_valid() ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 更新分组
|
||||
*
|
||||
* @param GroupModel $group 分组模型
|
||||
* @return bool
|
||||
*/
|
||||
public function update( GroupModel $group ): bool {
|
||||
if ( ! $group->is_valid() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = $this->get_all();
|
||||
$found = false;
|
||||
$groups = $this->get_all();
|
||||
$found = false;
|
||||
|
||||
foreach ( $groups as $index => $existing ) {
|
||||
if ( $existing->id === $group->id ) {
|
||||
$group->updated_at = current_time( 'mysql' );
|
||||
$group->created_at = $existing->created_at;
|
||||
foreach ( $groups as $index => $existing ) {
|
||||
if ( $existing->id === $group->id ) {
|
||||
$group->updated_at = current_time( 'mysql' );
|
||||
$group->created_at = $existing->created_at;
|
||||
|
||||
// 处理共享认证令牌
|
||||
if ( $group->shared_auth_token === '********' || empty( $group->shared_auth_token ) ) {
|
||||
$group->shared_auth_token = $existing->shared_auth_token;
|
||||
} else {
|
||||
$group->shared_auth_token = Encryption::encrypt( $group->shared_auth_token );
|
||||
}
|
||||
// 处理共享认证令牌
|
||||
if ( $group->shared_auth_token === '********' || empty( $group->shared_auth_token ) ) {
|
||||
$group->shared_auth_token = $existing->shared_auth_token;
|
||||
} else {
|
||||
$group->shared_auth_token = Encryption::encrypt( $group->shared_auth_token );
|
||||
}
|
||||
|
||||
$groups[ $index ] = $group;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$groups[ $index ] = $group;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $found ) {
|
||||
return false;
|
||||
}
|
||||
if ( ! $found ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::info( '更新源分组', array( 'id' => $group->id ) );
|
||||
Logger::info( '更新源分组', [ 'id' => $group->id ] );
|
||||
|
||||
return $this->save_groups( $groups );
|
||||
}
|
||||
return $this->save_groups( $groups );
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分组
|
||||
*
|
||||
* @param string $id 分组 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $id ): bool {
|
||||
$groups = $this->get_all();
|
||||
$new_groups = array();
|
||||
/**
|
||||
* 删除分组
|
||||
*
|
||||
* @param string $id 分组 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $id ): bool {
|
||||
$groups = $this->get_all();
|
||||
$new_groups = [];
|
||||
|
||||
foreach ( $groups as $group ) {
|
||||
if ( $group->id !== $id ) {
|
||||
$new_groups[] = $group;
|
||||
}
|
||||
}
|
||||
foreach ( $groups as $group ) {
|
||||
if ( $group->id !== $id ) {
|
||||
$new_groups[] = $group;
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $new_groups ) === count( $groups ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( count( $new_groups ) === count( $groups ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::info( '删除源分组', array( 'id' => $id ) );
|
||||
Logger::info( '删除源分组', [ 'id' => $id ] );
|
||||
|
||||
return $this->save_groups( $new_groups );
|
||||
}
|
||||
return $this->save_groups( $new_groups );
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换分组状态
|
||||
*
|
||||
* @param string $id 分组 ID
|
||||
* @param bool $enabled 是否启用
|
||||
* @return bool
|
||||
*/
|
||||
public function toggle( string $id, bool $enabled ): bool {
|
||||
$group = $this->get( $id );
|
||||
/**
|
||||
* 切换分组状态
|
||||
*
|
||||
* @param string $id 分组 ID
|
||||
* @param bool $enabled 是否启用
|
||||
* @return bool
|
||||
*/
|
||||
public function toggle( string $id, bool $enabled ): bool {
|
||||
$group = $this->get( $id );
|
||||
|
||||
if ( null === $group ) {
|
||||
return false;
|
||||
}
|
||||
if ( null === $group ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 先更新分组状态
|
||||
$group->enabled = $enabled;
|
||||
if ( ! $this->update( $group ) ) {
|
||||
return false;
|
||||
}
|
||||
// 先更新分组状态
|
||||
$group->enabled = $enabled;
|
||||
if ( ! $this->update( $group ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 然后批量更新源状态,记录失败的源
|
||||
$failed_sources = array();
|
||||
foreach ( $group->source_ids as $source_id ) {
|
||||
if ( ! $this->source_manager->toggle( $source_id, $enabled ) ) {
|
||||
$failed_sources[] = $source_id;
|
||||
}
|
||||
}
|
||||
// 然后批量更新源状态,记录失败的源
|
||||
$failed_sources = [];
|
||||
foreach ( $group->source_ids as $source_id ) {
|
||||
if ( ! $this->source_manager->toggle( $source_id, $enabled ) ) {
|
||||
$failed_sources[] = $source_id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $failed_sources ) ) {
|
||||
Logger::warning(
|
||||
'部分源状态切换失败',
|
||||
array(
|
||||
'group_id' => $id,
|
||||
'failed' => $failed_sources,
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( ! empty( $failed_sources ) ) {
|
||||
Logger::warning( '部分源状态切换失败', [
|
||||
'group_id' => $id,
|
||||
'failed' => $failed_sources,
|
||||
] );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组内的所有源
|
||||
*
|
||||
* @param string $group_id 分组 ID
|
||||
* @return array
|
||||
*/
|
||||
public function get_group_sources( string $group_id ): array {
|
||||
$group = $this->get( $group_id );
|
||||
/**
|
||||
* 获取分组内的所有源
|
||||
*
|
||||
* @param string $group_id 分组 ID
|
||||
* @return array
|
||||
*/
|
||||
public function get_group_sources( string $group_id ): array {
|
||||
$group = $this->get( $group_id );
|
||||
|
||||
if ( null === $group ) {
|
||||
return array();
|
||||
}
|
||||
if ( null === $group ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sources = array();
|
||||
foreach ( $group->source_ids as $source_id ) {
|
||||
$source = $this->source_manager->get( $source_id );
|
||||
if ( null !== $source ) {
|
||||
$sources[] = $source;
|
||||
}
|
||||
}
|
||||
$sources = [];
|
||||
foreach ( $group->source_ids as $source_id ) {
|
||||
$source = $this->source_manager->get( $source_id );
|
||||
if ( null !== $source ) {
|
||||
$sources[] = $source;
|
||||
}
|
||||
}
|
||||
|
||||
return $sources;
|
||||
}
|
||||
return $sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将源添加到分组
|
||||
*
|
||||
* @param string $group_id 分组 ID
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function add_source_to_group( string $group_id, string $source_id ): bool {
|
||||
// 权限检查
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 将源添加到分组
|
||||
*
|
||||
* @param string $group_id 分组 ID
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function add_source_to_group( string $group_id, string $source_id ): bool {
|
||||
// 权限检查
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$group = $this->get( $group_id );
|
||||
$group = $this->get( $group_id );
|
||||
|
||||
if ( null === $group ) {
|
||||
return false;
|
||||
}
|
||||
if ( null === $group ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$group->add_source( $source_id );
|
||||
$group->add_source( $source_id );
|
||||
|
||||
return $this->update( $group );
|
||||
}
|
||||
return $this->update( $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* 从分组移除源
|
||||
*
|
||||
* @param string $group_id 分组 ID
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function remove_source_from_group( string $group_id, string $source_id ): bool {
|
||||
// 权限检查
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 从分组移除源
|
||||
*
|
||||
* @param string $group_id 分组 ID
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function remove_source_from_group( string $group_id, string $source_id ): bool {
|
||||
// 权限检查
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$group = $this->get( $group_id );
|
||||
$group = $this->get( $group_id );
|
||||
|
||||
if ( null === $group ) {
|
||||
return false;
|
||||
}
|
||||
if ( null === $group ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$group->remove_source( $source_id );
|
||||
$group->remove_source( $source_id );
|
||||
|
||||
return $this->update( $group );
|
||||
}
|
||||
return $this->update( $group );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取源所属的分组
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return GroupModel[]
|
||||
*/
|
||||
public function get_source_groups( string $source_id ): array {
|
||||
$groups = $this->get_all();
|
||||
$source_groups = array();
|
||||
/**
|
||||
* 获取源所属的分组
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return GroupModel[]
|
||||
*/
|
||||
public function get_source_groups( string $source_id ): array {
|
||||
$groups = $this->get_all();
|
||||
$source_groups = [];
|
||||
|
||||
foreach ( $groups as $group ) {
|
||||
if ( $group->has_source( $source_id ) ) {
|
||||
$source_groups[] = $group;
|
||||
}
|
||||
}
|
||||
foreach ( $groups as $group ) {
|
||||
if ( $group->has_source( $source_id ) ) {
|
||||
$source_groups[] = $group;
|
||||
}
|
||||
}
|
||||
|
||||
return $source_groups;
|
||||
}
|
||||
return $source_groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存分组数据
|
||||
*
|
||||
* @param GroupModel[] $groups 分组列表
|
||||
* @return bool
|
||||
*/
|
||||
private function save_groups( array $groups ): bool {
|
||||
$data = array_map( fn( $g ) => $g->to_array(), $groups );
|
||||
return update_option( self::OPTION_NAME, $data );
|
||||
}
|
||||
/**
|
||||
* 保存分组数据
|
||||
*
|
||||
* @param GroupModel[] $groups 分组列表
|
||||
* @return bool
|
||||
*/
|
||||
private function save_groups( array $groups ): bool {
|
||||
$data = array_map( fn( $g ) => $g->to_array(), $groups );
|
||||
return update_option( self::OPTION_NAME, $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
$groups = $this->get_all();
|
||||
$total = count( $groups );
|
||||
$enabled = 0;
|
||||
$total_sources = 0;
|
||||
/**
|
||||
* 获取统计信息
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
$groups = $this->get_all();
|
||||
$total = count( $groups );
|
||||
$enabled = 0;
|
||||
$total_sources = 0;
|
||||
|
||||
foreach ( $groups as $group ) {
|
||||
if ( $group->enabled ) {
|
||||
++$enabled;
|
||||
}
|
||||
$total_sources += $group->get_source_count();
|
||||
}
|
||||
foreach ( $groups as $group ) {
|
||||
if ( $group->enabled ) {
|
||||
$enabled++;
|
||||
}
|
||||
$total_sources += $group->get_source_count();
|
||||
}
|
||||
|
||||
return array(
|
||||
'total' => $total,
|
||||
'enabled' => $enabled,
|
||||
'disabled' => $total - $enabled,
|
||||
'total_sources' => $total_sources,
|
||||
);
|
||||
}
|
||||
return [
|
||||
'total' => $total,
|
||||
'enabled' => $enabled,
|
||||
'disabled' => $total - $enabled,
|
||||
'total_sources' => $total_sources,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\SourceGroup;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,185 +17,183 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class GroupModel {
|
||||
|
||||
/**
|
||||
* 唯一标识
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $id = '';
|
||||
/**
|
||||
* 唯一标识
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $id = '';
|
||||
|
||||
/**
|
||||
* 分组名称
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $name = '';
|
||||
/**
|
||||
* 分组名称
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $name = '';
|
||||
|
||||
/**
|
||||
* 分组描述
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $description = '';
|
||||
/**
|
||||
* 分组描述
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $description = '';
|
||||
|
||||
/**
|
||||
* 包含的源 ID 列表
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $source_ids = array();
|
||||
/**
|
||||
* 包含的源 ID 列表
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $source_ids = [];
|
||||
|
||||
/**
|
||||
* 共享认证令牌
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $shared_auth_token = '';
|
||||
/**
|
||||
* 共享认证令牌
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $shared_auth_token = '';
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $enabled = true;
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $enabled = true;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $created_at = '';
|
||||
/**
|
||||
* 创建时间
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $created_at = '';
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $updated_at = '';
|
||||
/**
|
||||
* 更新时间
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $updated_at = '';
|
||||
|
||||
/**
|
||||
* 从数组创建实例
|
||||
*
|
||||
* @param array $data 数据数组
|
||||
* @return self
|
||||
*/
|
||||
public static function from_array( array $data ): self {
|
||||
$model = new self();
|
||||
/**
|
||||
* 从数组创建实例
|
||||
*
|
||||
* @param array $data 数据数组
|
||||
* @return self
|
||||
*/
|
||||
public static function from_array( array $data ): self {
|
||||
$model = new self();
|
||||
|
||||
$model->id = sanitize_text_field( $data['id'] ?? '' );
|
||||
$model->name = sanitize_text_field( $data['name'] ?? '' );
|
||||
$model->description = sanitize_textarea_field( $data['description'] ?? '' );
|
||||
$model->id = sanitize_text_field( $data['id'] ?? '' );
|
||||
$model->name = sanitize_text_field( $data['name'] ?? '' );
|
||||
$model->description = sanitize_textarea_field( $data['description'] ?? '' );
|
||||
|
||||
// 验证 source_ids 为字符串数组
|
||||
$source_ids = $data['source_ids'] ?? array();
|
||||
if ( is_array( $source_ids ) ) {
|
||||
$model->source_ids = array_values(
|
||||
array_filter(
|
||||
array_map( 'sanitize_text_field', $source_ids ),
|
||||
function ( $id ) {
|
||||
return ! empty( $id ) && is_string( $id );
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
// 验证 source_ids 为字符串数组
|
||||
$source_ids = $data['source_ids'] ?? [];
|
||||
if ( is_array( $source_ids ) ) {
|
||||
$model->source_ids = array_values( array_filter(
|
||||
array_map( 'sanitize_text_field', $source_ids ),
|
||||
function ( $id ) {
|
||||
return ! empty( $id ) && is_string( $id );
|
||||
}
|
||||
) );
|
||||
}
|
||||
|
||||
$model->shared_auth_token = $data['shared_auth_token'] ?? '';
|
||||
$model->enabled = (bool) ( $data['enabled'] ?? true );
|
||||
$model->created_at = sanitize_text_field( $data['created_at'] ?? '' );
|
||||
$model->updated_at = sanitize_text_field( $data['updated_at'] ?? '' );
|
||||
$model->shared_auth_token = $data['shared_auth_token'] ?? '';
|
||||
$model->enabled = (bool) ( $data['enabled'] ?? true );
|
||||
$model->created_at = sanitize_text_field( $data['created_at'] ?? '' );
|
||||
$model->updated_at = sanitize_text_field( $data['updated_at'] ?? '' );
|
||||
|
||||
return $model;
|
||||
}
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*
|
||||
* @param bool $include_sensitive 是否包含敏感字段
|
||||
* @return array
|
||||
*/
|
||||
public function to_array( bool $include_sensitive = true ): array {
|
||||
$data = array(
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'source_ids' => $this->source_ids,
|
||||
'enabled' => $this->enabled,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
);
|
||||
/**
|
||||
* 转换为数组
|
||||
*
|
||||
* @param bool $include_sensitive 是否包含敏感字段
|
||||
* @return array
|
||||
*/
|
||||
public function to_array( bool $include_sensitive = true ): array {
|
||||
$data = [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'source_ids' => $this->source_ids,
|
||||
'enabled' => $this->enabled,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
|
||||
$data['shared_auth_token'] = $include_sensitive
|
||||
? $this->shared_auth_token
|
||||
: ( empty( $this->shared_auth_token ) ? '' : '********' );
|
||||
$data['shared_auth_token'] = $include_sensitive
|
||||
? $this->shared_auth_token
|
||||
: ( empty( $this->shared_auth_token ) ? '' : '********' );
|
||||
|
||||
return $data;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证模型
|
||||
*
|
||||
* @return array 错误数组
|
||||
*/
|
||||
public function validate(): array {
|
||||
$errors = array();
|
||||
/**
|
||||
* 验证模型
|
||||
*
|
||||
* @return array 错误数组
|
||||
*/
|
||||
public function validate(): array {
|
||||
$errors = [];
|
||||
|
||||
if ( empty( $this->name ) ) {
|
||||
$errors['name'] = __( '分组名称不能为空', 'wpbridge' );
|
||||
}
|
||||
if ( empty( $this->name ) ) {
|
||||
$errors['name'] = __( '分组名称不能为空', 'wpbridge' );
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_valid(): bool {
|
||||
return empty( $this->validate() );
|
||||
}
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_valid(): bool {
|
||||
return empty( $this->validate() );
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加源到分组
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
public function add_source( string $source_id ): void {
|
||||
if ( ! in_array( $source_id, $this->source_ids, true ) ) {
|
||||
$this->source_ids[] = $source_id;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 添加源到分组
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
public function add_source( string $source_id ): void {
|
||||
if ( ! in_array( $source_id, $this->source_ids, true ) ) {
|
||||
$this->source_ids[] = $source_id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从分组移除源
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
public function remove_source( string $source_id ): void {
|
||||
$this->source_ids = array_values(
|
||||
array_filter(
|
||||
$this->source_ids,
|
||||
fn( $id ) => $id !== $source_id
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 从分组移除源
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
*/
|
||||
public function remove_source( string $source_id ): void {
|
||||
$this->source_ids = array_values(
|
||||
array_filter(
|
||||
$this->source_ids,
|
||||
fn( $id ) => $id !== $source_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查源是否在分组中
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function has_source( string $source_id ): bool {
|
||||
return in_array( $source_id, $this->source_ids, true );
|
||||
}
|
||||
/**
|
||||
* 检查源是否在分组中
|
||||
*
|
||||
* @param string $source_id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function has_source( string $source_id ): bool {
|
||||
return in_array( $source_id, $this->source_ids, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取源数量
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_source_count(): int {
|
||||
return count( $this->source_ids );
|
||||
}
|
||||
/**
|
||||
* 获取源数量
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_source_count(): int {
|
||||
return count( $this->source_ids );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use WPBridge\Security\Encryption;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -21,231 +21,219 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
abstract class AbstractHandler implements HandlerInterface {
|
||||
|
||||
/**
|
||||
* 源模型
|
||||
*
|
||||
* @var SourceModel
|
||||
*/
|
||||
protected SourceModel $source;
|
||||
/**
|
||||
* 源模型
|
||||
*
|
||||
* @var SourceModel
|
||||
*/
|
||||
protected SourceModel $source;
|
||||
|
||||
/**
|
||||
* 请求超时时间(秒)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $timeout = 10;
|
||||
/**
|
||||
* 请求超时时间(秒)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $timeout = 10;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
*/
|
||||
public function __construct( SourceModel $source ) {
|
||||
$this->source = $source;
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
*/
|
||||
public function __construct( SourceModel $source ) {
|
||||
$this->source = $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
'auth' => 'none',
|
||||
'version' => 'json',
|
||||
'download' => 'direct',
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return [
|
||||
'auth' => 'none',
|
||||
'version' => 'json',
|
||||
'download' => 'direct',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
return $this->source->api_url;
|
||||
}
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
return $this->source->api_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
return $this->source->get_headers();
|
||||
}
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
return $this->source->get_headers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证认证信息
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_auth(): bool {
|
||||
// 默认不需要认证
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 验证认证信息
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_auth(): bool {
|
||||
// 默认不需要认证
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试连通性
|
||||
*
|
||||
* @return HealthStatus
|
||||
*/
|
||||
public function test_connection(): HealthStatus {
|
||||
$start = microtime( true );
|
||||
/**
|
||||
* 测试连通性
|
||||
*
|
||||
* @return HealthStatus
|
||||
*/
|
||||
public function test_connection(): HealthStatus {
|
||||
$start = microtime( true );
|
||||
|
||||
$response = wp_remote_get(
|
||||
$this->get_check_url(),
|
||||
array(
|
||||
'timeout' => $this->timeout,
|
||||
'headers' => $this->get_headers(),
|
||||
)
|
||||
);
|
||||
$response = wp_remote_get( $this->get_check_url(), [
|
||||
'timeout' => $this->timeout,
|
||||
'headers' => $this->get_headers(),
|
||||
] );
|
||||
|
||||
$elapsed = (int) ( ( microtime( true ) - $start ) * 1000 );
|
||||
$elapsed = (int) ( ( microtime( true ) - $start ) * 1000 );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return HealthStatus::failed( $response->get_error_message() );
|
||||
}
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return HealthStatus::failed( $response->get_error_message() );
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( $code >= 200 && $code < 300 ) {
|
||||
return HealthStatus::healthy( $elapsed );
|
||||
}
|
||||
if ( $code >= 200 && $code < 300 ) {
|
||||
return HealthStatus::healthy( $elapsed );
|
||||
}
|
||||
|
||||
if ( $code >= 500 ) {
|
||||
return HealthStatus::failed( sprintf( 'HTTP %d', $code ) );
|
||||
}
|
||||
if ( $code >= 500 ) {
|
||||
return HealthStatus::failed( sprintf( 'HTTP %d', $code ) );
|
||||
}
|
||||
|
||||
return HealthStatus::degraded( $elapsed, sprintf( 'HTTP %d', $code ) );
|
||||
}
|
||||
return HealthStatus::degraded( $elapsed, sprintf( 'HTTP %d', $code ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起 HTTP 请求
|
||||
*
|
||||
* @param string $url URL
|
||||
* @param array $args 请求参数
|
||||
* @return array|null
|
||||
*/
|
||||
protected function request( string $url, array $args = array() ): ?array {
|
||||
$defaults = array(
|
||||
'timeout' => $this->timeout,
|
||||
'headers' => $this->get_headers(),
|
||||
);
|
||||
/**
|
||||
* 发起 HTTP 请求
|
||||
*
|
||||
* @param string $url URL
|
||||
* @param array $args 请求参数
|
||||
* @return array|null
|
||||
*/
|
||||
protected function request( string $url, array $args = [] ): ?array {
|
||||
$defaults = [
|
||||
'timeout' => $this->timeout,
|
||||
'headers' => $this->get_headers(),
|
||||
];
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
$response = wp_remote_get( $url, $args );
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
$response = wp_remote_get( $url, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error(
|
||||
'请求失败',
|
||||
array(
|
||||
'url' => $this->redact_url( $url ),
|
||||
'error' => $response->get_error_message(),
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error( '请求失败', [
|
||||
'url' => $this->redact_url( $url ),
|
||||
'error' => $response->get_error_message(),
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
|
||||
if ( $code < 200 || $code >= 300 ) {
|
||||
Logger::warning(
|
||||
'请求返回非 2xx 状态码',
|
||||
array(
|
||||
'url' => $this->redact_url( $url ),
|
||||
'code' => $code,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if ( $code < 200 || $code >= 300 ) {
|
||||
Logger::warning( '请求返回非 2xx 状态码', [
|
||||
'url' => $this->redact_url( $url ),
|
||||
'code' => $code,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode( $body, true );
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
Logger::error(
|
||||
'JSON 解析失败',
|
||||
array(
|
||||
'url' => $this->redact_url( $url ),
|
||||
'error' => json_last_error_msg(),
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
Logger::error( 'JSON 解析失败', [
|
||||
'url' => $this->redact_url( $url ),
|
||||
'error' => json_last_error_msg(),
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较版本号
|
||||
*
|
||||
* @param string $current 当前版本
|
||||
* @param string $remote 远程版本
|
||||
* @return bool 远程版本是否更新
|
||||
*/
|
||||
protected function is_newer_version( string $current, string $remote ): bool {
|
||||
return version_compare( $remote, $current, '>' );
|
||||
}
|
||||
/**
|
||||
* 比较版本号
|
||||
*
|
||||
* @param string $current 当前版本
|
||||
* @param string $remote 远程版本
|
||||
* @return bool 远程版本是否更新
|
||||
*/
|
||||
protected function is_newer_version( string $current, string $remote ): bool {
|
||||
return version_compare( $remote, $current, '>' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解密后的认证令牌
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_auth_token(): string {
|
||||
if ( empty( $this->source->auth_token ) ) {
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* 获取解密后的认证令牌
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_auth_token(): string {
|
||||
if ( empty( $this->source->auth_token ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$token = Encryption::decrypt( $this->source->auth_token );
|
||||
$token = Encryption::decrypt( $this->source->auth_token );
|
||||
|
||||
if ( empty( $token ) ) {
|
||||
if ( Encryption::is_encrypted( $this->source->auth_token ) ) {
|
||||
Logger::error( 'Token 解密失败', array( 'source' => $this->source->id ) );
|
||||
return '';
|
||||
}
|
||||
if ( empty( $token ) ) {
|
||||
if ( Encryption::is_encrypted( $this->source->auth_token ) ) {
|
||||
Logger::error( 'Token 解密失败', [ 'source' => $this->source->id ] );
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->source->auth_token;
|
||||
}
|
||||
return $this->source->auth_token;
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏 URL 中的敏感参数
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @return string
|
||||
*/
|
||||
protected function redact_url( string $url ): string {
|
||||
$parts = wp_parse_url( $url );
|
||||
if ( empty( $parts ) || empty( $parts['query'] ) ) {
|
||||
return $url;
|
||||
}
|
||||
/**
|
||||
* 脱敏 URL 中的敏感参数
|
||||
*
|
||||
* @param string $url 原始 URL
|
||||
* @return string
|
||||
*/
|
||||
protected function redact_url( string $url ): string {
|
||||
$parts = wp_parse_url( $url );
|
||||
if ( empty( $parts ) || empty( $parts['query'] ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
parse_str( $parts['query'], $query );
|
||||
if ( empty( $query ) ) {
|
||||
return $url;
|
||||
}
|
||||
parse_str( $parts['query'], $query );
|
||||
if ( empty( $query ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$sensitive_keys = array( 'access_token', 'api_key', 'token', 'key', 'auth', 'authorization' );
|
||||
foreach ( $query as $key => $value ) {
|
||||
if ( in_array( strtolower( (string) $key ), $sensitive_keys, true ) ) {
|
||||
$query[ $key ] = '***';
|
||||
}
|
||||
}
|
||||
$sensitive_keys = [ 'access_token', 'api_key', 'token', 'key', 'auth', 'authorization' ];
|
||||
foreach ( $query as $key => $value ) {
|
||||
if ( in_array( strtolower( (string) $key ), $sensitive_keys, true ) ) {
|
||||
$query[ $key ] = '***';
|
||||
}
|
||||
}
|
||||
|
||||
$scheme = isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '';
|
||||
$user = $parts['user'] ?? '';
|
||||
$pass = isset( $parts['pass'] ) ? ':' . $parts['pass'] : '';
|
||||
$auth = $user ? $user . $pass . '@' : '';
|
||||
$host = $parts['host'] ?? '';
|
||||
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
|
||||
$path = $parts['path'] ?? '';
|
||||
$querystr = http_build_query( $query );
|
||||
$fragment = isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
|
||||
$scheme = isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '';
|
||||
$user = $parts['user'] ?? '';
|
||||
$pass = isset( $parts['pass'] ) ? ':' . $parts['pass'] : '';
|
||||
$auth = $user ? $user . $pass . '@' : '';
|
||||
$host = $parts['host'] ?? '';
|
||||
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
|
||||
$path = $parts['path'] ?? '';
|
||||
$querystr = http_build_query( $query );
|
||||
$fragment = isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
|
||||
|
||||
return $scheme . $auth . $host . $port . $path . ( $querystr ? '?' . $querystr : '' ) . $fragment;
|
||||
}
|
||||
return $scheme . $auth . $host . $port . $path . ( $querystr ? '?' . $querystr : '' ) . $fragment;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20,182 +20,159 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class ArkPressHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
'auth' => 'token',
|
||||
'version' => 'json',
|
||||
'download' => 'direct',
|
||||
'federation' => true,
|
||||
'cdn' => true,
|
||||
'mirror' => true,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return [
|
||||
'auth' => 'token',
|
||||
'version' => 'json',
|
||||
'download' => 'direct',
|
||||
'federation' => true,
|
||||
'cdn' => true,
|
||||
'mirror' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
return rtrim( $this->source->api_url, '/' ) . '/plugins/update-check';
|
||||
}
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
return rtrim( $this->source->api_url, '/' ) . '/plugins/update-check';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$url = $this->build_plugin_url( $slug );
|
||||
$data = $this->request( $url );
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$url = $this->build_plugin_url( $slug );
|
||||
$data = $this->request( $url );
|
||||
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ArkPress API 响应格式
|
||||
$remote_version = $data['version'] ?? $data['new_version'] ?? '';
|
||||
// ArkPress API 响应格式
|
||||
$remote_version = $data['version'] ?? $data['new_version'] ?? '';
|
||||
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning(
|
||||
'ArkPress 响应缺少版本信息',
|
||||
array(
|
||||
'url' => $url,
|
||||
'slug' => $slug,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( 'ArkPress 响应缺少版本信息', [ 'url' => $url, 'slug' => $slug ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug(
|
||||
'ArkPress: 无可用更新',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug( 'ArkPress: 无可用更新', [
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建更新信息
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $data['download_url'] ?? $data['package'] ?? '';
|
||||
$info->details_url = $data['details_url'] ?? $data['url'] ?? '';
|
||||
$info->requires = $data['requires'] ?? '';
|
||||
$info->tested = $data['tested'] ?? '';
|
||||
$info->requires_php = $data['requires_php'] ?? '';
|
||||
$info->last_updated = $data['last_updated'] ?? '';
|
||||
$info->icons = $data['icons'] ?? array();
|
||||
$info->banners = $data['banners'] ?? array();
|
||||
// 构建更新信息
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $data['download_url'] ?? $data['package'] ?? '';
|
||||
$info->details_url = $data['details_url'] ?? $data['url'] ?? '';
|
||||
$info->requires = $data['requires'] ?? '';
|
||||
$info->tested = $data['tested'] ?? '';
|
||||
$info->requires_php = $data['requires_php'] ?? '';
|
||||
$info->last_updated = $data['last_updated'] ?? '';
|
||||
$info->icons = $data['icons'] ?? [];
|
||||
$info->banners = $data['banners'] ?? [];
|
||||
|
||||
if ( isset( $data['sections'] ) ) {
|
||||
$info->changelog = $data['sections']['changelog'] ?? '';
|
||||
$info->description = $data['sections']['description'] ?? '';
|
||||
}
|
||||
if ( isset( $data['sections'] ) ) {
|
||||
$info->changelog = $data['sections']['changelog'] ?? '';
|
||||
$info->description = $data['sections']['description'] ?? '';
|
||||
}
|
||||
|
||||
Logger::info(
|
||||
'ArkPress: 发现更新',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
)
|
||||
);
|
||||
Logger::info( 'ArkPress: 发现更新', [
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
] );
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$url = $this->build_plugin_url( $slug );
|
||||
return $this->request( $url );
|
||||
}
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$url = $this->build_plugin_url( $slug );
|
||||
return $this->request( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量检查更新
|
||||
*
|
||||
* @param array $plugins 插件列表 [ slug => version ]
|
||||
* @return array<string, UpdateInfo>
|
||||
*/
|
||||
public function check_updates_batch( array $plugins ): array {
|
||||
$url = rtrim( $this->source->api_url, '/' ) . '/plugins/update-check';
|
||||
/**
|
||||
* 批量检查更新
|
||||
*
|
||||
* @param array $plugins 插件列表 [ slug => version ]
|
||||
* @return array<string, UpdateInfo>
|
||||
*/
|
||||
public function check_updates_batch( array $plugins ): array {
|
||||
$url = rtrim( $this->source->api_url, '/' ) . '/plugins/update-check';
|
||||
|
||||
$response = wp_remote_post(
|
||||
$url,
|
||||
array(
|
||||
'timeout' => $this->timeout,
|
||||
'headers' => array_merge(
|
||||
$this->get_headers(),
|
||||
array(
|
||||
'Content-Type' => 'application/json',
|
||||
)
|
||||
),
|
||||
'body' => wp_json_encode(
|
||||
array(
|
||||
'plugins' => $plugins,
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
$response = wp_remote_post( $url, [
|
||||
'timeout' => $this->timeout,
|
||||
'headers' => array_merge( $this->get_headers(), [
|
||||
'Content-Type' => 'application/json',
|
||||
] ),
|
||||
'body' => wp_json_encode( [
|
||||
'plugins' => $plugins,
|
||||
] ),
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error(
|
||||
'ArkPress 批量检查失败',
|
||||
array(
|
||||
'error' => $response->get_error_message(),
|
||||
)
|
||||
);
|
||||
return array();
|
||||
}
|
||||
if ( is_wp_error( $response ) ) {
|
||||
Logger::error( 'ArkPress 批量检查失败', [
|
||||
'error' => $response->get_error_message(),
|
||||
] );
|
||||
return [];
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
if ( ! is_array( $data ) || ! isset( $data['plugins'] ) ) {
|
||||
return array();
|
||||
}
|
||||
if ( ! is_array( $data ) || ! isset( $data['plugins'] ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$updates = array();
|
||||
foreach ( $data['plugins'] as $slug => $plugin_data ) {
|
||||
if ( empty( $plugin_data['version'] ) ) {
|
||||
continue;
|
||||
}
|
||||
$updates = [];
|
||||
foreach ( $data['plugins'] as $slug => $plugin_data ) {
|
||||
if ( empty( $plugin_data['version'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$current = $plugins[ $slug ] ?? '0.0.0';
|
||||
if ( $this->is_newer_version( $current, $plugin_data['version'] ) ) {
|
||||
$info = UpdateInfo::from_array( $plugin_data );
|
||||
$info->slug = $slug;
|
||||
$updates[ $slug ] = $info;
|
||||
}
|
||||
}
|
||||
$current = $plugins[ $slug ] ?? '0.0.0';
|
||||
if ( $this->is_newer_version( $current, $plugin_data['version'] ) ) {
|
||||
$info = UpdateInfo::from_array( $plugin_data );
|
||||
$info->slug = $slug;
|
||||
$updates[ $slug ] = $info;
|
||||
}
|
||||
}
|
||||
|
||||
return $updates;
|
||||
}
|
||||
return $updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建插件 URL
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return string
|
||||
*/
|
||||
protected function build_plugin_url( string $slug ): string {
|
||||
return rtrim( $this->source->api_url, '/' ) . '/plugins/' . $slug;
|
||||
}
|
||||
/**
|
||||
* 构建插件 URL
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return string
|
||||
*/
|
||||
protected function build_plugin_url( string $slug ): string {
|
||||
return rtrim( $this->source->api_url, '/' ) . '/plugins/' . $slug;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,106 +19,94 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class AspireCloudHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
'auth' => 'token',
|
||||
'version' => 'json',
|
||||
'download' => 'direct',
|
||||
'federation' => true,
|
||||
'cdn' => true,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return [
|
||||
'auth' => 'token',
|
||||
'version' => 'json',
|
||||
'download' => 'direct',
|
||||
'federation' => true,
|
||||
'cdn' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
return rtrim( $this->source->api_url, '/' ) . '/plugins/update-check/1.1';
|
||||
}
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
return rtrim( $this->source->api_url, '/' ) . '/plugins/update-check/1.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$url = $this->build_plugin_url( $slug );
|
||||
$data = $this->request( $url );
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$url = $this->build_plugin_url( $slug );
|
||||
$data = $this->request( $url );
|
||||
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// AspireCloud API 响应格式
|
||||
$remote_version = $data['version'] ?? $data['new_version'] ?? '';
|
||||
// AspireCloud API 响应格式
|
||||
$remote_version = $data['version'] ?? $data['new_version'] ?? '';
|
||||
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning(
|
||||
'AspireCloud 响应缺少版本信息',
|
||||
array(
|
||||
'url' => $url,
|
||||
'slug' => $slug,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( 'AspireCloud 响应缺少版本信息', [ 'url' => $url, 'slug' => $slug ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug(
|
||||
'AspireCloud: 无可用更新',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug( 'AspireCloud: 无可用更新', [
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建更新信息
|
||||
$info = UpdateInfo::from_array( $data );
|
||||
$info->slug = $slug;
|
||||
// 构建更新信息
|
||||
$info = UpdateInfo::from_array( $data );
|
||||
$info->slug = $slug;
|
||||
|
||||
Logger::info(
|
||||
'AspireCloud: 发现更新',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
)
|
||||
);
|
||||
Logger::info( 'AspireCloud: 发现更新', [
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
] );
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$url = $this->build_plugin_url( $slug );
|
||||
return $this->request( $url );
|
||||
}
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$url = $this->build_plugin_url( $slug );
|
||||
return $this->request( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建插件 URL
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return string
|
||||
*/
|
||||
protected function build_plugin_url( string $slug ): string {
|
||||
return rtrim( $this->source->api_url, '/' ) . '/plugins/info/1.2?slug=' . urlencode( $slug );
|
||||
}
|
||||
/**
|
||||
* 构建插件 URL
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return string
|
||||
*/
|
||||
protected function build_plugin_url( string $slug ): string {
|
||||
return rtrim( $this->source->api_url, '/' ) . '/plugins/info/1.2?slug=' . urlencode( $slug );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,12 +56,12 @@ class BridgeServerHandler extends AbstractHandler {
|
|||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
return [
|
||||
'auth' => 'api_key',
|
||||
'version' => 'json',
|
||||
'download' => 'signed_url',
|
||||
'batch' => true,
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -82,7 +82,7 @@ class BridgeServerHandler extends AbstractHandler {
|
|||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
if ( ! $this->client || ! $this->client->is_configured() ) {
|
||||
Logger::warning( 'Bridge Server 未配置', array( 'slug' => $slug ) );
|
||||
Logger::warning( 'Bridge Server 未配置', [ 'slug' => $slug ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -101,26 +101,24 @@ class BridgeServerHandler extends AbstractHandler {
|
|||
$download_url = $this->client->get_download_url( $slug );
|
||||
|
||||
if ( empty( $download_url ) ) {
|
||||
Logger::warning( 'Bridge Server 无法获取下载 URL', array( 'slug' => $slug ) );
|
||||
Logger::warning( 'Bridge Server 无法获取下载 URL', [ 'slug' => $slug ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
return UpdateInfo::from_array(
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'version' => $info['version'],
|
||||
'download_url' => $download_url,
|
||||
'details_url' => $info['homepage'] ?? '',
|
||||
'requires' => $info['requires'] ?? '',
|
||||
'tested' => $info['tested'] ?? '',
|
||||
'requires_php' => $info['requires_php'] ?? '',
|
||||
'last_updated' => $info['updated_at'] ?? '',
|
||||
'icons' => $info['icons'] ?? array(),
|
||||
'banners' => $info['banners'] ?? array(),
|
||||
'changelog' => $info['changelog'] ?? '',
|
||||
'description' => $info['description'] ?? '',
|
||||
)
|
||||
);
|
||||
return UpdateInfo::from_array( [
|
||||
'slug' => $slug,
|
||||
'version' => $info['version'],
|
||||
'download_url' => $download_url,
|
||||
'details_url' => $info['homepage'] ?? '',
|
||||
'requires' => $info['requires'] ?? '',
|
||||
'tested' => $info['tested'] ?? '',
|
||||
'requires_php' => $info['requires_php'] ?? '',
|
||||
'last_updated' => $info['updated_at'] ?? '',
|
||||
'icons' => $info['icons'] ?? [],
|
||||
'banners' => $info['banners'] ?? [],
|
||||
'changelog' => $info['changelog'] ?? '',
|
||||
'description' => $info['description'] ?? '',
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use WPBridge\FAIR\FairSourceAdapter;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20,95 +20,95 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class FairHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
'auth' => 'token',
|
||||
'version' => 'fair',
|
||||
'download' => 'direct',
|
||||
'signature' => 'ed25519',
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return [
|
||||
'auth' => 'token',
|
||||
'version' => 'fair',
|
||||
'download' => 'direct',
|
||||
'signature' => 'ed25519',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$adapter = $this->get_adapter();
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$adapter = $this->get_adapter();
|
||||
|
||||
$data = $this->source->item_type === 'theme'
|
||||
? $adapter->check_theme_update( $slug, $version )
|
||||
: $adapter->check_plugin_update( $slug, $version );
|
||||
$data = $this->source->item_type === 'theme'
|
||||
? $adapter->check_theme_update( $slug, $version )
|
||||
: $adapter->check_plugin_update( $slug, $version );
|
||||
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$info = UpdateInfo::from_array( $data );
|
||||
$info->slug = $slug;
|
||||
$info = UpdateInfo::from_array( $data );
|
||||
$info->slug = $slug;
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$adapter = $this->get_adapter();
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$adapter = $this->get_adapter();
|
||||
|
||||
return $this->source->item_type === 'theme'
|
||||
? $adapter->get_theme_info( $slug )
|
||||
: $adapter->get_plugin_info( $slug );
|
||||
}
|
||||
return $this->source->item_type === 'theme'
|
||||
? $adapter->get_theme_info( $slug )
|
||||
: $adapter->get_plugin_info( $slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 FAIR 适配器
|
||||
*
|
||||
* @return FairSourceAdapter
|
||||
*/
|
||||
private function get_adapter(): FairSourceAdapter {
|
||||
return new FairSourceAdapter( $this->build_source_config() );
|
||||
}
|
||||
/**
|
||||
* 获取 FAIR 适配器
|
||||
*
|
||||
* @return FairSourceAdapter
|
||||
*/
|
||||
private function get_adapter(): FairSourceAdapter {
|
||||
return new FairSourceAdapter( $this->build_source_config() );
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 FAIR 源配置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function build_source_config(): array {
|
||||
$headers = array(
|
||||
'Accept' => 'application/json',
|
||||
);
|
||||
/**
|
||||
* 构建 FAIR 源配置
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function build_source_config(): array {
|
||||
$headers = [
|
||||
'Accept' => 'application/json',
|
||||
];
|
||||
|
||||
$token = $this->get_auth_token();
|
||||
if ( ! empty( $token ) ) {
|
||||
$scheme = $this->source->metadata['auth_scheme'] ?? '';
|
||||
if ( $scheme === 'bearer' ) {
|
||||
$headers['Authorization'] = 'Bearer ' . $token;
|
||||
} elseif ( $scheme === 'basic' ) {
|
||||
$headers['Authorization'] = 'Basic ' . base64_encode( $token );
|
||||
} else {
|
||||
$headers['Authorization'] = 'Token ' . $token;
|
||||
}
|
||||
}
|
||||
$token = $this->get_auth_token();
|
||||
if ( ! empty( $token ) ) {
|
||||
$scheme = $this->source->metadata['auth_scheme'] ?? '';
|
||||
if ( $scheme === 'bearer' ) {
|
||||
$headers['Authorization'] = 'Bearer ' . $token;
|
||||
} elseif ( $scheme === 'basic' ) {
|
||||
$headers['Authorization'] = 'Basic ' . base64_encode( $token );
|
||||
} else {
|
||||
$headers['Authorization'] = 'Token ' . $token;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'api_url' => $this->source->api_url,
|
||||
'auth_type' => SourceRegistry::AUTH_NONE,
|
||||
'auth_secret_ref' => '',
|
||||
'signature_required' => (bool) ( $this->source->metadata['signature_required'] ?? false ),
|
||||
'headers' => $headers,
|
||||
);
|
||||
}
|
||||
return [
|
||||
'api_url' => $this->source->api_url,
|
||||
'auth_type' => SourceRegistry::AUTH_NONE,
|
||||
'auth_secret_ref' => '',
|
||||
'signature_required' => (bool) ( $this->source->metadata['signature_required'] ?? false ),
|
||||
'headers' => $headers,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,226 +19,220 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class GitHubHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* GitHub API 基础 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const API_BASE = 'https://api.github.com';
|
||||
/**
|
||||
* GitHub API 基础 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const API_BASE = 'https://api.github.com';
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
'auth' => 'token',
|
||||
'version' => 'release',
|
||||
'download' => 'release',
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return [
|
||||
'auth' => 'token',
|
||||
'version' => 'release',
|
||||
'download' => 'release',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
$headers = parent::get_headers();
|
||||
$headers['Accept'] = 'application/vnd.github.v3+json';
|
||||
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
|
||||
return $headers;
|
||||
}
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
$headers = parent::get_headers();
|
||||
$headers['Accept'] = 'application/vnd.github.v3+json';
|
||||
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
if ( empty( $repo ) ) {
|
||||
return $this->source->api_url;
|
||||
}
|
||||
return self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
}
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
if ( empty( $repo ) ) {
|
||||
return $this->source->api_url;
|
||||
}
|
||||
return self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
|
||||
if ( empty( $repo ) ) {
|
||||
Logger::warning( 'GitHub: 无效的仓库 URL', array( 'url' => $this->source->api_url ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $repo ) ) {
|
||||
Logger::warning( 'GitHub: 无效的仓库 URL', [ 'url' => $this->source->api_url ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
$data = $this->request( $url );
|
||||
$url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
$data = $this->request( $url );
|
||||
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析版本号(去除 v 前缀)
|
||||
$remote_version = $data['tag_name'] ?? '';
|
||||
$remote_version = ltrim( $remote_version, 'v' );
|
||||
// 解析版本号(去除 v 前缀)
|
||||
$remote_version = $data['tag_name'] ?? '';
|
||||
$remote_version = ltrim( $remote_version, 'v' );
|
||||
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( 'GitHub: 响应缺少版本信息', array( 'repo' => $repo ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( 'GitHub: 响应缺少版本信息', [ 'repo' => $repo ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug(
|
||||
'GitHub: 无可用更新',
|
||||
array(
|
||||
'repo' => $repo,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug( 'GitHub: 无可用更新', [
|
||||
'repo' => $repo,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查找下载 URL
|
||||
$download_url = $this->find_download_url( $data, $slug );
|
||||
// 查找下载 URL
|
||||
$download_url = $this->find_download_url( $data, $slug );
|
||||
|
||||
if ( empty( $download_url ) ) {
|
||||
Logger::warning( 'GitHub: 未找到下载 URL', array( 'repo' => $repo ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $download_url ) ) {
|
||||
Logger::warning( 'GitHub: 未找到下载 URL', [ 'repo' => $repo ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建更新信息
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $download_url;
|
||||
$info->details_url = $data['html_url'] ?? '';
|
||||
$info->last_updated = $data['published_at'] ?? '';
|
||||
$info->changelog = $data['body'] ?? '';
|
||||
// 构建更新信息
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $download_url;
|
||||
$info->details_url = $data['html_url'] ?? '';
|
||||
$info->last_updated = $data['published_at'] ?? '';
|
||||
$info->changelog = $data['body'] ?? '';
|
||||
|
||||
Logger::info(
|
||||
'GitHub: 发现更新',
|
||||
array(
|
||||
'repo' => $repo,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
)
|
||||
);
|
||||
Logger::info( 'GitHub: 发现更新', [
|
||||
'repo' => $repo,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
] );
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
|
||||
if ( empty( $repo ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $repo ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取仓库信息
|
||||
$repo_url = self::API_BASE . '/repos/' . $repo;
|
||||
$repo_data = $this->request( $repo_url );
|
||||
// 获取仓库信息
|
||||
$repo_url = self::API_BASE . '/repos/' . $repo;
|
||||
$repo_data = $this->request( $repo_url );
|
||||
|
||||
// 获取最新 Release
|
||||
$release_url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
$release_data = $this->request( $release_url );
|
||||
// 获取最新 Release
|
||||
$release_url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
$release_data = $this->request( $release_url );
|
||||
|
||||
if ( null === $repo_data || null === $release_data ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $repo_data || null === $release_data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$version = ltrim( $release_data['tag_name'] ?? '', 'v' );
|
||||
$version = ltrim( $release_data['tag_name'] ?? '', 'v' );
|
||||
|
||||
return array(
|
||||
'name' => $repo_data['name'] ?? $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $version,
|
||||
'download_url' => $this->find_download_url( $release_data, $slug ),
|
||||
'details_url' => $release_data['html_url'] ?? '',
|
||||
'last_updated' => $release_data['published_at'] ?? '',
|
||||
'sections' => array(
|
||||
'description' => $repo_data['description'] ?? '',
|
||||
'changelog' => $release_data['body'] ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
return [
|
||||
'name' => $repo_data['name'] ?? $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $version,
|
||||
'download_url' => $this->find_download_url( $release_data, $slug ),
|
||||
'details_url' => $release_data['html_url'] ?? '',
|
||||
'last_updated' => $release_data['published_at'] ?? '',
|
||||
'sections' => [
|
||||
'description' => $repo_data['description'] ?? '',
|
||||
'changelog' => $release_data['body'] ?? '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析仓库 URL
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return string|null owner/repo 格式
|
||||
*/
|
||||
private function parse_repo_url( string $url ): ?string {
|
||||
// 支持多种格式
|
||||
// https://github.com/owner/repo
|
||||
// github.com/owner/repo
|
||||
// owner/repo
|
||||
/**
|
||||
* 解析仓库 URL
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return string|null owner/repo 格式
|
||||
*/
|
||||
private function parse_repo_url( string $url ): ?string {
|
||||
// 支持多种格式
|
||||
// https://github.com/owner/repo
|
||||
// github.com/owner/repo
|
||||
// owner/repo
|
||||
|
||||
$url = trim( $url );
|
||||
$url = trim( $url );
|
||||
|
||||
// 移除协议
|
||||
$url = preg_replace( '#^https?://#', '', $url );
|
||||
// 移除协议
|
||||
$url = preg_replace( '#^https?://#', '', $url );
|
||||
|
||||
// 移除 github.com
|
||||
$url = preg_replace( '#^github\.com/#', '', $url );
|
||||
// 移除 github.com
|
||||
$url = preg_replace( '#^github\.com/#', '', $url );
|
||||
|
||||
// 移除 .git 后缀
|
||||
$url = preg_replace( '#\.git$#', '', $url );
|
||||
// 移除 .git 后缀
|
||||
$url = preg_replace( '#\.git$#', '', $url );
|
||||
|
||||
// 验证格式
|
||||
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
|
||||
return $url;
|
||||
}
|
||||
// 验证格式
|
||||
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找下载 URL
|
||||
*
|
||||
* @param array $release Release 数据
|
||||
* @param string $slug 插件 slug
|
||||
* @return string|null
|
||||
*/
|
||||
private function find_download_url( array $release, string $slug ): ?string {
|
||||
// 优先查找 assets 中的 zip 文件
|
||||
if ( ! empty( $release['assets'] ) ) {
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
$name = $asset['name'] ?? '';
|
||||
/**
|
||||
* 查找下载 URL
|
||||
*
|
||||
* @param array $release Release 数据
|
||||
* @param string $slug 插件 slug
|
||||
* @return string|null
|
||||
*/
|
||||
private function find_download_url( array $release, string $slug ): ?string {
|
||||
// 优先查找 assets 中的 zip 文件
|
||||
if ( ! empty( $release['assets'] ) ) {
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
$name = $asset['name'] ?? '';
|
||||
|
||||
// 匹配 slug.zip 或 slug-version.zip
|
||||
if ( preg_match( '/\.zip$/i', $name ) ) {
|
||||
if ( stripos( $name, $slug ) !== false ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 匹配 slug.zip 或 slug-version.zip
|
||||
if ( preg_match( '/\.zip$/i', $name ) ) {
|
||||
if ( stripos( $name, $slug ) !== false ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配 slug 的,返回第一个 zip
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果没有匹配 slug 的,返回第一个 zip
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 zipball_url 作为后备
|
||||
return $release['zipball_url'] ?? null;
|
||||
}
|
||||
// 使用 zipball_url 作为后备
|
||||
return $release['zipball_url'] ?? null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,238 +19,232 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class GitLabHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* GitLab API 基础 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const API_BASE = 'https://gitlab.com/api/v4';
|
||||
/**
|
||||
* GitLab API 基础 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const API_BASE = 'https://gitlab.com/api/v4';
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
'auth' => 'token',
|
||||
'version' => 'release',
|
||||
'download' => 'release',
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return [
|
||||
'auth' => 'token',
|
||||
'version' => 'release',
|
||||
'download' => 'release',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
$headers = array();
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
$headers = [];
|
||||
|
||||
$token = $this->get_auth_token();
|
||||
if ( ! empty( $token ) ) {
|
||||
// GitLab 使用 PRIVATE-TOKEN 头
|
||||
$headers['PRIVATE-TOKEN'] = $token;
|
||||
}
|
||||
$token = $this->get_auth_token();
|
||||
if ( ! empty( $token ) ) {
|
||||
// GitLab 使用 PRIVATE-TOKEN 头
|
||||
$headers['PRIVATE-TOKEN'] = $token;
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$project_id = $this->get_project_id();
|
||||
if ( empty( $project_id ) ) {
|
||||
return $this->source->api_url;
|
||||
}
|
||||
return self::API_BASE . '/projects/' . $project_id . '/releases';
|
||||
}
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$project_id = $this->get_project_id();
|
||||
if ( empty( $project_id ) ) {
|
||||
return $this->source->api_url;
|
||||
}
|
||||
return self::API_BASE . '/projects/' . $project_id . '/releases';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$project_id = $this->get_project_id();
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$project_id = $this->get_project_id();
|
||||
|
||||
if ( empty( $project_id ) ) {
|
||||
Logger::warning( 'GitLab: 无效的项目 URL', array( 'url' => $this->source->api_url ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $project_id ) ) {
|
||||
Logger::warning( 'GitLab: 无效的项目 URL', [ 'url' => $this->source->api_url ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = self::API_BASE . '/projects/' . $project_id . '/releases';
|
||||
$data = $this->request( $url );
|
||||
$url = self::API_BASE . '/projects/' . $project_id . '/releases';
|
||||
$data = $this->request( $url );
|
||||
|
||||
if ( null === $data || empty( $data ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $data || empty( $data ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取最新 Release(第一个)
|
||||
$latest = $data[0] ?? null;
|
||||
// 获取最新 Release(第一个)
|
||||
$latest = $data[0] ?? null;
|
||||
|
||||
if ( null === $latest ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $latest ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析版本号
|
||||
$remote_version = $latest['tag_name'] ?? '';
|
||||
$remote_version = ltrim( $remote_version, 'v' );
|
||||
// 解析版本号
|
||||
$remote_version = $latest['tag_name'] ?? '';
|
||||
$remote_version = ltrim( $remote_version, 'v' );
|
||||
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( 'GitLab: 响应缺少版本信息', array( 'project' => $project_id ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( 'GitLab: 响应缺少版本信息', [ 'project' => $project_id ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug(
|
||||
'GitLab: 无可用更新',
|
||||
array(
|
||||
'project' => $project_id,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug( 'GitLab: 无可用更新', [
|
||||
'project' => $project_id,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查找下载 URL
|
||||
$download_url = $this->find_download_url( $latest, $slug, $project_id );
|
||||
// 查找下载 URL
|
||||
$download_url = $this->find_download_url( $latest, $slug, $project_id );
|
||||
|
||||
if ( empty( $download_url ) ) {
|
||||
Logger::warning( 'GitLab: 未找到下载 URL', array( 'project' => $project_id ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $download_url ) ) {
|
||||
Logger::warning( 'GitLab: 未找到下载 URL', [ 'project' => $project_id ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建更新信息
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $download_url;
|
||||
$info->details_url = $latest['_links']['self'] ?? '';
|
||||
$info->last_updated = $latest['released_at'] ?? '';
|
||||
$info->changelog = $latest['description'] ?? '';
|
||||
// 构建更新信息
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $download_url;
|
||||
$info->details_url = $latest['_links']['self'] ?? '';
|
||||
$info->last_updated = $latest['released_at'] ?? '';
|
||||
$info->changelog = $latest['description'] ?? '';
|
||||
|
||||
Logger::info(
|
||||
'GitLab: 发现更新',
|
||||
array(
|
||||
'project' => $project_id,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
)
|
||||
);
|
||||
Logger::info( 'GitLab: 发现更新', [
|
||||
'project' => $project_id,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
] );
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$project_id = $this->get_project_id();
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$project_id = $this->get_project_id();
|
||||
|
||||
if ( empty( $project_id ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $project_id ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取项目信息
|
||||
$project_url = self::API_BASE . '/projects/' . $project_id;
|
||||
$project_data = $this->request( $project_url );
|
||||
// 获取项目信息
|
||||
$project_url = self::API_BASE . '/projects/' . $project_id;
|
||||
$project_data = $this->request( $project_url );
|
||||
|
||||
// 获取 Releases
|
||||
$releases_url = self::API_BASE . '/projects/' . $project_id . '/releases';
|
||||
$releases_data = $this->request( $releases_url );
|
||||
// 获取 Releases
|
||||
$releases_url = self::API_BASE . '/projects/' . $project_id . '/releases';
|
||||
$releases_data = $this->request( $releases_url );
|
||||
|
||||
if ( null === $project_data ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $project_data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$latest = $releases_data[0] ?? array();
|
||||
$version = ltrim( $latest['tag_name'] ?? '', 'v' );
|
||||
$latest = $releases_data[0] ?? [];
|
||||
$version = ltrim( $latest['tag_name'] ?? '', 'v' );
|
||||
|
||||
return array(
|
||||
'name' => $project_data['name'] ?? $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $version,
|
||||
'download_url' => $this->find_download_url( $latest, $slug, $project_id ),
|
||||
'details_url' => $project_data['web_url'] ?? '',
|
||||
'last_updated' => $latest['released_at'] ?? '',
|
||||
'sections' => array(
|
||||
'description' => $project_data['description'] ?? '',
|
||||
'changelog' => $latest['description'] ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
return [
|
||||
'name' => $project_data['name'] ?? $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $version,
|
||||
'download_url' => $this->find_download_url( $latest, $slug, $project_id ),
|
||||
'details_url' => $project_data['web_url'] ?? '',
|
||||
'last_updated' => $latest['released_at'] ?? '',
|
||||
'sections' => [
|
||||
'description' => $project_data['description'] ?? '',
|
||||
'changelog' => $latest['description'] ?? '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目 ID(URL 编码的路径)
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function get_project_id(): ?string {
|
||||
$url = trim( $this->source->api_url );
|
||||
/**
|
||||
* 获取项目 ID(URL 编码的路径)
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function get_project_id(): ?string {
|
||||
$url = trim( $this->source->api_url );
|
||||
|
||||
// 移除协议
|
||||
$url = preg_replace( '#^https?://#', '', $url );
|
||||
// 移除协议
|
||||
$url = preg_replace( '#^https?://#', '', $url );
|
||||
|
||||
// 移除 gitlab.com
|
||||
$url = preg_replace( '#^gitlab\.com/#', '', $url );
|
||||
// 移除 gitlab.com
|
||||
$url = preg_replace( '#^gitlab\.com/#', '', $url );
|
||||
|
||||
// 移除 .git 后缀
|
||||
$url = preg_replace( '#\.git$#', '', $url );
|
||||
// 移除 .git 后缀
|
||||
$url = preg_replace( '#\.git$#', '', $url );
|
||||
|
||||
// URL 编码路径
|
||||
if ( preg_match( '#^[\w.-]+/[\w.-]+(?:/[\w.-]+)*$#', $url ) ) {
|
||||
return urlencode( $url );
|
||||
}
|
||||
// URL 编码路径
|
||||
if ( preg_match( '#^[\w.-]+/[\w.-]+(?:/[\w.-]+)*$#', $url ) ) {
|
||||
return urlencode( $url );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找下载 URL
|
||||
*
|
||||
* @param array $release Release 数据
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $project_id 项目 ID
|
||||
* @return string|null
|
||||
*/
|
||||
private function find_download_url( array $release, string $slug, string $project_id ): ?string {
|
||||
// 查找 assets 中的链接
|
||||
if ( ! empty( $release['assets']['links'] ) ) {
|
||||
foreach ( $release['assets']['links'] as $link ) {
|
||||
$name = $link['name'] ?? '';
|
||||
/**
|
||||
* 查找下载 URL
|
||||
*
|
||||
* @param array $release Release 数据
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $project_id 项目 ID
|
||||
* @return string|null
|
||||
*/
|
||||
private function find_download_url( array $release, string $slug, string $project_id ): ?string {
|
||||
// 查找 assets 中的链接
|
||||
if ( ! empty( $release['assets']['links'] ) ) {
|
||||
foreach ( $release['assets']['links'] as $link ) {
|
||||
$name = $link['name'] ?? '';
|
||||
|
||||
if ( preg_match( '/\.zip$/i', $name ) ) {
|
||||
if ( stripos( $name, $slug ) !== false ) {
|
||||
return $link['url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( preg_match( '/\.zip$/i', $name ) ) {
|
||||
if ( stripos( $name, $slug ) !== false ) {
|
||||
return $link['url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回第一个 zip
|
||||
foreach ( $release['assets']['links'] as $link ) {
|
||||
if ( preg_match( '/\.zip$/i', $link['name'] ?? '' ) ) {
|
||||
return $link['url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 返回第一个 zip
|
||||
foreach ( $release['assets']['links'] as $link ) {
|
||||
if ( preg_match( '/\.zip$/i', $link['name'] ?? '' ) ) {
|
||||
return $link['url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用归档 URL 作为后备
|
||||
$tag = $release['tag_name'] ?? '';
|
||||
if ( ! empty( $tag ) ) {
|
||||
return self::API_BASE . '/projects/' . $project_id . '/repository/archive.zip?sha=' . $tag;
|
||||
}
|
||||
// 使用归档 URL 作为后备
|
||||
$tag = $release['tag_name'] ?? '';
|
||||
if ( ! empty( $tag ) ) {
|
||||
return self::API_BASE . '/projects/' . $project_id . '/repository/archive.zip?sha=' . $tag;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,240 +19,234 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class GiteeHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* Gitee API 基础 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const API_BASE = 'https://gitee.com/api/v5';
|
||||
/**
|
||||
* Gitee API 基础 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const API_BASE = 'https://gitee.com/api/v5';
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
'auth' => 'token',
|
||||
'version' => 'release',
|
||||
'download' => 'release',
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return [
|
||||
'auth' => 'token',
|
||||
'version' => 'release',
|
||||
'download' => 'release',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
$headers = parent::get_headers();
|
||||
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
|
||||
return $headers;
|
||||
}
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
$headers = parent::get_headers();
|
||||
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
if ( empty( $repo ) ) {
|
||||
return $this->source->api_url;
|
||||
}
|
||||
return self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
}
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
if ( empty( $repo ) ) {
|
||||
return $this->source->api_url;
|
||||
}
|
||||
return self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
|
||||
if ( empty( $repo ) ) {
|
||||
Logger::warning( 'Gitee: 无效的仓库 URL', array( 'url' => $this->source->api_url ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $repo ) ) {
|
||||
Logger::warning( 'Gitee: 无效的仓库 URL', [ 'url' => $this->source->api_url ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建 URL(带 access_token 参数)
|
||||
$url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
// 构建 URL(带 access_token 参数)
|
||||
$url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
|
||||
$token = $this->get_auth_token();
|
||||
if ( ! empty( $token ) ) {
|
||||
$url = add_query_arg( 'access_token', $token, $url );
|
||||
}
|
||||
$token = $this->get_auth_token();
|
||||
if ( ! empty( $token ) ) {
|
||||
$url = add_query_arg( 'access_token', $token, $url );
|
||||
}
|
||||
|
||||
$data = $this->request( $url );
|
||||
$data = $this->request( $url );
|
||||
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析版本号
|
||||
$remote_version = $data['tag_name'] ?? '';
|
||||
$remote_version = ltrim( $remote_version, 'v' );
|
||||
// 解析版本号
|
||||
$remote_version = $data['tag_name'] ?? '';
|
||||
$remote_version = ltrim( $remote_version, 'v' );
|
||||
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( 'Gitee: 响应缺少版本信息', array( 'repo' => $repo ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( 'Gitee: 响应缺少版本信息', [ 'repo' => $repo ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug(
|
||||
'Gitee: 无可用更新',
|
||||
array(
|
||||
'repo' => $repo,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug( 'Gitee: 无可用更新', [
|
||||
'repo' => $repo,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查找下载 URL
|
||||
$download_url = $this->find_download_url( $data, $slug, $repo, $remote_version );
|
||||
// 查找下载 URL
|
||||
$download_url = $this->find_download_url( $data, $slug, $repo, $remote_version );
|
||||
|
||||
if ( empty( $download_url ) ) {
|
||||
Logger::warning( 'Gitee: 未找到下载 URL', array( 'repo' => $repo ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $download_url ) ) {
|
||||
Logger::warning( 'Gitee: 未找到下载 URL', [ 'repo' => $repo ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建更新信息
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $download_url;
|
||||
$info->details_url = 'https://gitee.com/' . $repo . '/releases/tag/' . $data['tag_name'];
|
||||
$info->last_updated = $data['created_at'] ?? '';
|
||||
$info->changelog = $data['body'] ?? '';
|
||||
// 构建更新信息
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $download_url;
|
||||
$info->details_url = 'https://gitee.com/' . $repo . '/releases/tag/' . $data['tag_name'];
|
||||
$info->last_updated = $data['created_at'] ?? '';
|
||||
$info->changelog = $data['body'] ?? '';
|
||||
|
||||
Logger::info(
|
||||
'Gitee: 发现更新',
|
||||
array(
|
||||
'repo' => $repo,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
)
|
||||
);
|
||||
Logger::info( 'Gitee: 发现更新', [
|
||||
'repo' => $repo,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
] );
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
|
||||
if ( empty( $repo ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $repo ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取仓库信息
|
||||
$repo_url = self::API_BASE . '/repos/' . $repo;
|
||||
$token = $this->get_auth_token();
|
||||
if ( ! empty( $token ) ) {
|
||||
$repo_url = add_query_arg( 'access_token', $token, $repo_url );
|
||||
}
|
||||
$repo_data = $this->request( $repo_url );
|
||||
// 获取仓库信息
|
||||
$repo_url = self::API_BASE . '/repos/' . $repo;
|
||||
$token = $this->get_auth_token();
|
||||
if ( ! empty( $token ) ) {
|
||||
$repo_url = add_query_arg( 'access_token', $token, $repo_url );
|
||||
}
|
||||
$repo_data = $this->request( $repo_url );
|
||||
|
||||
// 获取最新 Release
|
||||
$release_url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
if ( ! empty( $token ) ) {
|
||||
$release_url = add_query_arg( 'access_token', $token, $release_url );
|
||||
}
|
||||
$release_data = $this->request( $release_url );
|
||||
// 获取最新 Release
|
||||
$release_url = self::API_BASE . '/repos/' . $repo . '/releases/latest';
|
||||
if ( ! empty( $token ) ) {
|
||||
$release_url = add_query_arg( 'access_token', $token, $release_url );
|
||||
}
|
||||
$release_data = $this->request( $release_url );
|
||||
|
||||
if ( null === $repo_data ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $repo_data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$version = ltrim( $release_data['tag_name'] ?? '', 'v' );
|
||||
$version = ltrim( $release_data['tag_name'] ?? '', 'v' );
|
||||
|
||||
return array(
|
||||
'name' => $repo_data['name'] ?? $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $version,
|
||||
'download_url' => $this->find_download_url( $release_data ?? array(), $slug, $repo, $version ),
|
||||
'details_url' => $repo_data['html_url'] ?? '',
|
||||
'last_updated' => $release_data['created_at'] ?? '',
|
||||
'sections' => array(
|
||||
'description' => $repo_data['description'] ?? '',
|
||||
'changelog' => $release_data['body'] ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
return [
|
||||
'name' => $repo_data['name'] ?? $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $version,
|
||||
'download_url' => $this->find_download_url( $release_data ?? [], $slug, $repo, $version ),
|
||||
'details_url' => $repo_data['html_url'] ?? '',
|
||||
'last_updated' => $release_data['created_at'] ?? '',
|
||||
'sections' => [
|
||||
'description' => $repo_data['description'] ?? '',
|
||||
'changelog' => $release_data['body'] ?? '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析仓库 URL
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return string|null owner/repo 格式
|
||||
*/
|
||||
private function parse_repo_url( string $url ): ?string {
|
||||
$url = trim( $url );
|
||||
/**
|
||||
* 解析仓库 URL
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return string|null owner/repo 格式
|
||||
*/
|
||||
private function parse_repo_url( string $url ): ?string {
|
||||
$url = trim( $url );
|
||||
|
||||
// 移除协议
|
||||
$url = preg_replace( '#^https?://#', '', $url );
|
||||
// 移除协议
|
||||
$url = preg_replace( '#^https?://#', '', $url );
|
||||
|
||||
// 移除 gitee.com
|
||||
$url = preg_replace( '#^gitee\.com/#', '', $url );
|
||||
// 移除 gitee.com
|
||||
$url = preg_replace( '#^gitee\.com/#', '', $url );
|
||||
|
||||
// 移除 .git 后缀
|
||||
$url = preg_replace( '#\.git$#', '', $url );
|
||||
// 移除 .git 后缀
|
||||
$url = preg_replace( '#\.git$#', '', $url );
|
||||
|
||||
// 验证格式
|
||||
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
|
||||
return $url;
|
||||
}
|
||||
// 验证格式
|
||||
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找下载 URL
|
||||
*
|
||||
* @param array $release Release 数据
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $repo 仓库路径
|
||||
* @param string $version 版本号
|
||||
* @return string|null
|
||||
*/
|
||||
private function find_download_url( array $release, string $slug, string $repo, string $version ): ?string {
|
||||
// 查找 assets 中的 zip 文件
|
||||
if ( ! empty( $release['assets'] ) ) {
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
$name = $asset['name'] ?? '';
|
||||
/**
|
||||
* 查找下载 URL
|
||||
*
|
||||
* @param array $release Release 数据
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $repo 仓库路径
|
||||
* @param string $version 版本号
|
||||
* @return string|null
|
||||
*/
|
||||
private function find_download_url( array $release, string $slug, string $repo, string $version ): ?string {
|
||||
// 查找 assets 中的 zip 文件
|
||||
if ( ! empty( $release['assets'] ) ) {
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
$name = $asset['name'] ?? '';
|
||||
|
||||
if ( preg_match( '/\.zip$/i', $name ) ) {
|
||||
if ( stripos( $name, $slug ) !== false ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( preg_match( '/\.zip$/i', $name ) ) {
|
||||
if ( stripos( $name, $slug ) !== false ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回第一个 zip
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 返回第一个 zip
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用归档 URL 作为后备
|
||||
$tag = $release['tag_name'] ?? '';
|
||||
if ( ! empty( $tag ) ) {
|
||||
return 'https://gitee.com/' . $repo . '/repository/archive/' . $tag . '.zip';
|
||||
}
|
||||
// 使用归档 URL 作为后备
|
||||
$tag = $release['tag_name'] ?? '';
|
||||
if ( ! empty( $tag ) ) {
|
||||
return 'https://gitee.com/' . $repo . '/repository/archive/' . $tag . '.zip';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\UpdateSource\SourceModel;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,64 +19,64 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
interface HandlerInterface {
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
*/
|
||||
public function __construct( SourceModel $source );
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
*/
|
||||
public function __construct( SourceModel $source );
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array;
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array;
|
||||
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string;
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string;
|
||||
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array;
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array;
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo;
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo;
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array;
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array;
|
||||
|
||||
/**
|
||||
* 验证认证信息
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_auth(): bool;
|
||||
/**
|
||||
* 验证认证信息
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_auth(): bool;
|
||||
|
||||
/**
|
||||
* 测试连通性
|
||||
*
|
||||
* @return HealthStatus
|
||||
*/
|
||||
public function test_connection(): HealthStatus;
|
||||
/**
|
||||
* 测试连通性
|
||||
*
|
||||
* @return HealthStatus
|
||||
*/
|
||||
public function test_connection(): HealthStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -84,158 +84,158 @@ interface HandlerInterface {
|
|||
*/
|
||||
class UpdateInfo {
|
||||
|
||||
/**
|
||||
* 插件/主题 slug
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $slug = '';
|
||||
/**
|
||||
* 插件/主题 slug
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $slug = '';
|
||||
|
||||
/**
|
||||
* 新版本号
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $version = '';
|
||||
/**
|
||||
* 新版本号
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $version = '';
|
||||
|
||||
/**
|
||||
* 下载 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $download_url = '';
|
||||
/**
|
||||
* 下载 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $download_url = '';
|
||||
|
||||
/**
|
||||
* 详情 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $details_url = '';
|
||||
/**
|
||||
* 详情 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $details_url = '';
|
||||
|
||||
/**
|
||||
* 最低 WordPress 版本
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $requires = '';
|
||||
/**
|
||||
* 最低 WordPress 版本
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $requires = '';
|
||||
|
||||
/**
|
||||
* 测试通过的 WordPress 版本
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $tested = '';
|
||||
/**
|
||||
* 测试通过的 WordPress 版本
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $tested = '';
|
||||
|
||||
/**
|
||||
* 最低 PHP 版本
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $requires_php = '';
|
||||
/**
|
||||
* 最低 PHP 版本
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $requires_php = '';
|
||||
|
||||
/**
|
||||
* 最后更新时间
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $last_updated = '';
|
||||
/**
|
||||
* 最后更新时间
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $last_updated = '';
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $icons = array();
|
||||
/**
|
||||
* 图标
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $icons = [];
|
||||
|
||||
/**
|
||||
* 横幅
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $banners = array();
|
||||
/**
|
||||
* 横幅
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $banners = [];
|
||||
|
||||
/**
|
||||
* 更新日志
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $changelog = '';
|
||||
/**
|
||||
* 更新日志
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $changelog = '';
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $description = '';
|
||||
/**
|
||||
* 描述
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $description = '';
|
||||
|
||||
/**
|
||||
* 从数组创建
|
||||
*
|
||||
* @param array $data 数据
|
||||
* @return self
|
||||
*/
|
||||
public static function from_array( array $data ): self {
|
||||
$info = new self();
|
||||
/**
|
||||
* 从数组创建
|
||||
*
|
||||
* @param array $data 数据
|
||||
* @return self
|
||||
*/
|
||||
public static function from_array( array $data ): self {
|
||||
$info = new self();
|
||||
|
||||
$info->slug = $data['slug'] ?? '';
|
||||
$info->version = $data['version'] ?? '';
|
||||
$info->download_url = $data['download_url'] ?? $data['package'] ?? $data['download_link'] ?? '';
|
||||
$info->details_url = $data['details_url'] ?? $data['url'] ?? '';
|
||||
$info->requires = $data['requires'] ?? '';
|
||||
$info->tested = $data['tested'] ?? '';
|
||||
$info->requires_php = $data['requires_php'] ?? '';
|
||||
$info->last_updated = $data['last_updated'] ?? '';
|
||||
$info->icons = $data['icons'] ?? array();
|
||||
$info->banners = $data['banners'] ?? array();
|
||||
$info->changelog = $data['changelog'] ?? $data['sections']['changelog'] ?? '';
|
||||
$info->description = $data['description'] ?? $data['sections']['description'] ?? '';
|
||||
$info->slug = $data['slug'] ?? '';
|
||||
$info->version = $data['version'] ?? '';
|
||||
$info->download_url = $data['download_url'] ?? $data['package'] ?? $data['download_link'] ?? '';
|
||||
$info->details_url = $data['details_url'] ?? $data['url'] ?? '';
|
||||
$info->requires = $data['requires'] ?? '';
|
||||
$info->tested = $data['tested'] ?? '';
|
||||
$info->requires_php = $data['requires_php'] ?? '';
|
||||
$info->last_updated = $data['last_updated'] ?? '';
|
||||
$info->icons = $data['icons'] ?? [];
|
||||
$info->banners = $data['banners'] ?? [];
|
||||
$info->changelog = $data['changelog'] ?? $data['sections']['changelog'] ?? '';
|
||||
$info->description = $data['description'] ?? $data['sections']['description'] ?? '';
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 WordPress 更新对象格式
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function to_wp_update_object(): object {
|
||||
return (object) array(
|
||||
'slug' => $this->slug,
|
||||
'new_version' => $this->version,
|
||||
'package' => $this->download_url,
|
||||
'url' => $this->details_url,
|
||||
'requires' => $this->requires,
|
||||
'tested' => $this->tested,
|
||||
'requires_php' => $this->requires_php,
|
||||
'icons' => $this->icons,
|
||||
'banners' => $this->banners,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 转换为 WordPress 更新对象格式
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function to_wp_update_object(): object {
|
||||
return (object) [
|
||||
'slug' => $this->slug,
|
||||
'new_version' => $this->version,
|
||||
'package' => $this->download_url,
|
||||
'url' => $this->details_url,
|
||||
'requires' => $this->requires,
|
||||
'tested' => $this->tested,
|
||||
'requires_php' => $this->requires_php,
|
||||
'icons' => $this->icons,
|
||||
'banners' => $this->banners,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 plugins_api 响应格式
|
||||
*
|
||||
* @param string $name 插件名称
|
||||
* @return object
|
||||
*/
|
||||
public function to_plugins_api_response( string $name = '' ): object {
|
||||
return (object) array(
|
||||
'name' => $name ?: $this->slug,
|
||||
'slug' => $this->slug,
|
||||
'version' => $this->version,
|
||||
'download_link' => $this->download_url,
|
||||
'requires' => $this->requires,
|
||||
'tested' => $this->tested,
|
||||
'requires_php' => $this->requires_php,
|
||||
'last_updated' => $this->last_updated,
|
||||
'sections' => array(
|
||||
'description' => $this->description,
|
||||
'changelog' => $this->changelog,
|
||||
),
|
||||
'icons' => $this->icons,
|
||||
'banners' => $this->banners,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 转换为 plugins_api 响应格式
|
||||
*
|
||||
* @param string $name 插件名称
|
||||
* @return object
|
||||
*/
|
||||
public function to_plugins_api_response( string $name = '' ): object {
|
||||
return (object) [
|
||||
'name' => $name ?: $this->slug,
|
||||
'slug' => $this->slug,
|
||||
'version' => $this->version,
|
||||
'download_link' => $this->download_url,
|
||||
'requires' => $this->requires,
|
||||
'tested' => $this->tested,
|
||||
'requires_php' => $this->requires_php,
|
||||
'last_updated' => $this->last_updated,
|
||||
'sections' => [
|
||||
'description' => $this->description,
|
||||
'changelog' => $this->changelog,
|
||||
],
|
||||
'icons' => $this->icons,
|
||||
'banners' => $this->banners,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -243,97 +243,97 @@ class UpdateInfo {
|
|||
*/
|
||||
class HealthStatus {
|
||||
|
||||
const STATUS_HEALTHY = 'healthy';
|
||||
const STATUS_DEGRADED = 'degraded';
|
||||
const STATUS_FAILED = 'failed';
|
||||
const STATUS_HEALTHY = 'healthy';
|
||||
const STATUS_DEGRADED = 'degraded';
|
||||
const STATUS_FAILED = 'failed';
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $status = self::STATUS_FAILED;
|
||||
/**
|
||||
* 状态
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $status = self::STATUS_FAILED;
|
||||
|
||||
/**
|
||||
* 响应时间(毫秒)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public int $response_time = 0;
|
||||
/**
|
||||
* 响应时间(毫秒)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public int $response_time = 0;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $error = '';
|
||||
/**
|
||||
* 错误信息
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $error = '';
|
||||
|
||||
/**
|
||||
* 检查时间
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public int $checked_at = 0;
|
||||
/**
|
||||
* 检查时间
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public int $checked_at = 0;
|
||||
|
||||
/**
|
||||
* 创建健康状态
|
||||
*
|
||||
* @param int $response_time 响应时间
|
||||
* @return self
|
||||
*/
|
||||
public static function healthy( int $response_time ): self {
|
||||
$status = new self();
|
||||
$status->status = self::STATUS_HEALTHY;
|
||||
$status->response_time = $response_time;
|
||||
$status->checked_at = time();
|
||||
return $status;
|
||||
}
|
||||
/**
|
||||
* 创建健康状态
|
||||
*
|
||||
* @param int $response_time 响应时间
|
||||
* @return self
|
||||
*/
|
||||
public static function healthy( int $response_time ): self {
|
||||
$status = new self();
|
||||
$status->status = self::STATUS_HEALTHY;
|
||||
$status->response_time = $response_time;
|
||||
$status->checked_at = time();
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建降级状态
|
||||
*
|
||||
* @param int $response_time 响应时间
|
||||
* @param string $reason 原因
|
||||
* @return self
|
||||
*/
|
||||
public static function degraded( int $response_time, string $reason = '' ): self {
|
||||
$status = new self();
|
||||
$status->status = self::STATUS_DEGRADED;
|
||||
$status->response_time = $response_time;
|
||||
$status->error = $reason;
|
||||
$status->checked_at = time();
|
||||
return $status;
|
||||
}
|
||||
/**
|
||||
* 创建降级状态
|
||||
*
|
||||
* @param int $response_time 响应时间
|
||||
* @param string $reason 原因
|
||||
* @return self
|
||||
*/
|
||||
public static function degraded( int $response_time, string $reason = '' ): self {
|
||||
$status = new self();
|
||||
$status->status = self::STATUS_DEGRADED;
|
||||
$status->response_time = $response_time;
|
||||
$status->error = $reason;
|
||||
$status->checked_at = time();
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建失败状态
|
||||
*
|
||||
* @param string $error 错误信息
|
||||
* @return self
|
||||
*/
|
||||
public static function failed( string $error ): self {
|
||||
$status = new self();
|
||||
$status->status = self::STATUS_FAILED;
|
||||
$status->error = $error;
|
||||
$status->checked_at = time();
|
||||
return $status;
|
||||
}
|
||||
/**
|
||||
* 创建失败状态
|
||||
*
|
||||
* @param string $error 错误信息
|
||||
* @return self
|
||||
*/
|
||||
public static function failed( string $error ): self {
|
||||
$status = new self();
|
||||
$status->status = self::STATUS_FAILED;
|
||||
$status->error = $error;
|
||||
$status->checked_at = time();
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否健康
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_healthy(): bool {
|
||||
return $this->status === self::STATUS_HEALTHY;
|
||||
}
|
||||
/**
|
||||
* 是否健康
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_healthy(): bool {
|
||||
return $this->status === self::STATUS_HEALTHY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可用(健康或降级)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_available(): bool {
|
||||
return $this->status !== self::STATUS_FAILED;
|
||||
}
|
||||
/**
|
||||
* 是否可用(健康或降级)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_available(): bool {
|
||||
return $this->status !== self::STATUS_FAILED;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\UpdateSource\Handlers;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/HandlerInterface.php';
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20,128 +20,122 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class JsonHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
'auth' => 'token',
|
||||
'version' => 'json',
|
||||
'download' => 'direct',
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return [
|
||||
'auth' => 'token',
|
||||
'version' => 'json',
|
||||
'download' => 'direct',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取健康检查 URL
|
||||
*
|
||||
* 对于包含 {slug} 模板的 URL,提取基础 URL 进行健康检查
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$url = $this->source->api_url;
|
||||
/**
|
||||
* 获取健康检查 URL
|
||||
*
|
||||
* 对于包含 {slug} 模板的 URL,提取基础 URL 进行健康检查
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$url = $this->source->api_url;
|
||||
|
||||
// 如果 URL 包含 {slug} 模板,提取基础 URL
|
||||
if ( strpos( $url, '{slug}' ) !== false ) {
|
||||
// 从 https://updates.wenpai.net/api/v1/plugins/{slug}/info
|
||||
// 提取 https://updates.wenpai.net/
|
||||
$parsed = wp_parse_url( $url );
|
||||
if ( $parsed && isset( $parsed['scheme'], $parsed['host'] ) ) {
|
||||
$base_url = $parsed['scheme'] . '://' . $parsed['host'];
|
||||
if ( isset( $parsed['port'] ) ) {
|
||||
$base_url .= ':' . $parsed['port'];
|
||||
}
|
||||
return $base_url . '/';
|
||||
}
|
||||
}
|
||||
// 如果 URL 包含 {slug} 模板,提取基础 URL
|
||||
if ( strpos( $url, '{slug}' ) !== false ) {
|
||||
// 从 https://updates.wenpai.net/api/v1/plugins/{slug}/info
|
||||
// 提取 https://updates.wenpai.net/
|
||||
$parsed = wp_parse_url( $url );
|
||||
if ( $parsed && isset( $parsed['scheme'], $parsed['host'] ) ) {
|
||||
$base_url = $parsed['scheme'] . '://' . $parsed['host'];
|
||||
if ( isset( $parsed['port'] ) ) {
|
||||
$base_url .= ':' . $parsed['port'];
|
||||
}
|
||||
return $base_url . '/';
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$url = $this->build_check_url( $slug );
|
||||
$data = $this->request( $url );
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$url = $this->build_check_url( $slug );
|
||||
$data = $this->request( $url );
|
||||
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理 Plugin Update Checker 格式
|
||||
$remote_version = $data['version'] ?? '';
|
||||
// 处理 Plugin Update Checker 格式
|
||||
$remote_version = $data['version'] ?? '';
|
||||
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( 'JSON 响应缺少版本信息', array( 'url' => $url ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( 'JSON 响应缺少版本信息', [ 'url' => $url ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug(
|
||||
'无可用更新',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// 检查是否有更新
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
Logger::debug( '无可用更新', [
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'remote' => $remote_version,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建更新信息
|
||||
$info = UpdateInfo::from_array( $data );
|
||||
$info->slug = $slug;
|
||||
// 构建更新信息
|
||||
$info = UpdateInfo::from_array( $data );
|
||||
$info->slug = $slug;
|
||||
|
||||
Logger::info(
|
||||
'发现更新',
|
||||
array(
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
)
|
||||
);
|
||||
Logger::info( '发现更新', [
|
||||
'slug' => $slug,
|
||||
'current' => $version,
|
||||
'new' => $remote_version,
|
||||
] );
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$url = $this->build_check_url( $slug );
|
||||
return $this->request( $url );
|
||||
}
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$url = $this->build_check_url( $slug );
|
||||
return $this->request( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建检查 URL
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return string
|
||||
*/
|
||||
protected function build_check_url( string $slug ): string {
|
||||
$url = $this->source->api_url;
|
||||
/**
|
||||
* 构建检查 URL
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return string
|
||||
*/
|
||||
protected function build_check_url( string $slug ): string {
|
||||
$url = $this->source->api_url;
|
||||
|
||||
// 如果 URL 包含占位符,替换它
|
||||
if ( strpos( $url, '{slug}' ) !== false ) {
|
||||
return str_replace( '{slug}', $slug, $url );
|
||||
}
|
||||
// 如果 URL 包含占位符,替换它
|
||||
if ( strpos( $url, '{slug}' ) !== false ) {
|
||||
return str_replace( '{slug}', $slug, $url );
|
||||
}
|
||||
|
||||
// 如果 URL 包含查询参数占位符
|
||||
if ( strpos( $url, '?' ) !== false ) {
|
||||
return add_query_arg( 'slug', $slug, $url );
|
||||
}
|
||||
// 如果 URL 包含查询参数占位符
|
||||
if ( strpos( $url, '?' ) !== false ) {
|
||||
return add_query_arg( 'slug', $slug, $url );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\UpdateSource\Handlers;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\UpdateSource\Handlers;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/HandlerInterface.php';
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,282 +19,282 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class WenPaiGitHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* API 基础 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const API_BASE = 'https://git.wenpai.org/api/v1';
|
||||
/**
|
||||
* API 基础 URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const API_BASE = 'https://git.wenpai.org/api/v1';
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
'auth' => 'token',
|
||||
'version' => 'release',
|
||||
'download' => 'release',
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return [
|
||||
'auth' => 'token',
|
||||
'version' => 'release',
|
||||
'download' => 'release',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
$headers = parent::get_headers();
|
||||
$headers['Accept'] = 'application/json';
|
||||
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
|
||||
return $headers;
|
||||
}
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
$headers = parent::get_headers();
|
||||
$headers['Accept'] = 'application/json';
|
||||
$headers['User-Agent'] = 'WPBridge/' . WPBRIDGE_VERSION;
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
if ( empty( $repo ) ) {
|
||||
return $this->source->api_url;
|
||||
}
|
||||
return $this->get_api_base() . '/repos/' . $repo . '/releases';
|
||||
}
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
if ( empty( $repo ) ) {
|
||||
return $this->source->api_url;
|
||||
}
|
||||
return $this->get_api_base() . '/repos/' . $repo . '/releases';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
|
||||
if ( empty( $repo ) ) {
|
||||
Logger::warning( '菲码源库: 无效的仓库 URL', array( 'url' => $this->source->api_url ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $repo ) ) {
|
||||
Logger::warning( '菲码源库: 无效的仓库 URL', [ 'url' => $this->source->api_url ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = $this->get_api_base() . '/repos/' . $repo . '/releases';
|
||||
$data = $this->request( $url );
|
||||
$url = $this->get_api_base() . '/repos/' . $repo . '/releases';
|
||||
$data = $this->request( $url );
|
||||
|
||||
if ( null === $data || empty( $data ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $data || empty( $data ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$latest = $data[0] ?? null;
|
||||
if ( null === $latest ) {
|
||||
return null;
|
||||
}
|
||||
$latest = $data[0] ?? null;
|
||||
if ( null === $latest ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$remote_version = ltrim( $latest['tag_name'] ?? '', 'v' );
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( '菲码源库: 响应缺少版本信息', array( 'repo' => $repo ) );
|
||||
return null;
|
||||
}
|
||||
$remote_version = ltrim( $latest['tag_name'] ?? '', 'v' );
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::warning( '菲码源库: 响应缺少版本信息', [ 'repo' => $repo ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$download_url = $this->find_download_url( $latest, $slug, $repo );
|
||||
if ( empty( $download_url ) ) {
|
||||
Logger::warning( '菲码源库: 未找到下载 URL', array( 'repo' => $repo ) );
|
||||
return null;
|
||||
}
|
||||
$download_url = $this->find_download_url( $latest, $slug, $repo );
|
||||
if ( empty( $download_url ) ) {
|
||||
Logger::warning( '菲码源库: 未找到下载 URL', [ 'repo' => $repo ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $download_url;
|
||||
$info->details_url = $latest['html_url'] ?? '';
|
||||
$info->last_updated = $latest['published_at'] ?? $latest['created_at'] ?? '';
|
||||
$info->changelog = $latest['body'] ?? '';
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $download_url;
|
||||
$info->details_url = $latest['html_url'] ?? '';
|
||||
$info->last_updated = $latest['published_at'] ?? $latest['created_at'] ?? '';
|
||||
$info->changelog = $latest['body'] ?? '';
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
if ( empty( $repo ) ) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
if ( empty( $repo ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$repo_url = $this->get_api_base() . '/repos/' . $repo;
|
||||
$repo_data = $this->request( $repo_url );
|
||||
$releases_url = $this->get_api_base() . '/repos/' . $repo . '/releases';
|
||||
$release_data = $this->request( $releases_url );
|
||||
$repo_url = $this->get_api_base() . '/repos/' . $repo;
|
||||
$repo_data = $this->request( $repo_url );
|
||||
$releases_url = $this->get_api_base() . '/repos/' . $repo . '/releases';
|
||||
$release_data = $this->request( $releases_url );
|
||||
|
||||
if ( null === $repo_data ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $repo_data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$latest = $release_data[0] ?? array();
|
||||
$version = ltrim( $latest['tag_name'] ?? '', 'v' );
|
||||
$latest = $release_data[0] ?? [];
|
||||
$version = ltrim( $latest['tag_name'] ?? '', 'v' );
|
||||
|
||||
return array(
|
||||
'name' => $repo_data['name'] ?? $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $version,
|
||||
'download_url' => $this->find_download_url( $latest, $slug, $repo ),
|
||||
'details_url' => $repo_data['html_url'] ?? '',
|
||||
'last_updated' => $latest['published_at'] ?? $latest['created_at'] ?? '',
|
||||
'sections' => array(
|
||||
'description' => $repo_data['description'] ?? '',
|
||||
'changelog' => $latest['body'] ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
return [
|
||||
'name' => $repo_data['name'] ?? $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $version,
|
||||
'download_url' => $this->find_download_url( $latest, $slug, $repo ),
|
||||
'details_url' => $repo_data['html_url'] ?? '',
|
||||
'last_updated' => $latest['published_at'] ?? $latest['created_at'] ?? '',
|
||||
'sections' => [
|
||||
'description' => $repo_data['description'] ?? '',
|
||||
'changelog' => $latest['body'] ?? '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析仓库 URL
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return string|null owner/repo 格式
|
||||
*/
|
||||
private function parse_repo_url( string $url ): ?string {
|
||||
$url = trim( $url );
|
||||
/**
|
||||
* 解析仓库 URL
|
||||
*
|
||||
* @param string $url URL
|
||||
* @return string|null owner/repo 格式
|
||||
*/
|
||||
private function parse_repo_url( string $url ): ?string {
|
||||
$url = trim( $url );
|
||||
|
||||
$parts = wp_parse_url( $url );
|
||||
if ( ! empty( $parts['host'] ) ) {
|
||||
$path = trim( $parts['path'] ?? '', '/' );
|
||||
$path = preg_replace( '#\.git$#', '', $path );
|
||||
$parts = wp_parse_url( $url );
|
||||
if ( ! empty( $parts['host'] ) ) {
|
||||
$path = trim( $parts['path'] ?? '', '/' );
|
||||
$path = preg_replace( '#\.git$#', '', $path );
|
||||
|
||||
if ( preg_match( '#^api/v1/repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
if ( preg_match( '#^api/v1/repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
if ( preg_match( '#^repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
if ( preg_match( '#^repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
if (
|
||||
( preg_match( '#^api/v1(?:/|$)#', $path ) && ! preg_match( '#^api/v1/repos/[\w.-]+/[\w.-]+#', $path ) )
|
||||
|| ( preg_match( '#^repos(?:/|$)#', $path ) && ! preg_match( '#^repos/[\w.-]+/[\w.-]+#', $path ) )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
( preg_match( '#^api/v1(?:/|$)#', $path ) && ! preg_match( '#^api/v1/repos/[\w.-]+/[\w.-]+#', $path ) )
|
||||
|| ( preg_match( '#^repos(?:/|$)#', $path ) && ! preg_match( '#^repos/[\w.-]+/[\w.-]+#', $path ) )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$segments = array_values( array_filter( explode( '/', $path ) ) );
|
||||
if ( count( $segments ) >= 2 ) {
|
||||
$repo = $segments[0] . '/' . $segments[1];
|
||||
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $repo ) ) {
|
||||
return $repo;
|
||||
}
|
||||
}
|
||||
$segments = array_values( array_filter( explode( '/', $path ) ) );
|
||||
if ( count( $segments ) >= 2 ) {
|
||||
$repo = $segments[0] . '/' . $segments[1];
|
||||
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $repo ) ) {
|
||||
return $repo;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = preg_replace( '#^https?://#', '', $url );
|
||||
$path = preg_replace( '#^[^/]+/#', '', $path );
|
||||
$path = trim( $path, '/' );
|
||||
$path = preg_replace( '#\.git$#', '', $path );
|
||||
$path = preg_replace( '#^https?://#', '', $url );
|
||||
$path = preg_replace( '#^[^/]+/#', '', $path );
|
||||
$path = trim( $path, '/' );
|
||||
$path = preg_replace( '#\.git$#', '', $path );
|
||||
|
||||
if ( preg_match( '#^api/v1/repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
if ( preg_match( '#^api/v1/repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
if ( preg_match( '#^repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
if ( preg_match( '#^repos/([\w.-]+/[\w.-]+)#', $path, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
if (
|
||||
( preg_match( '#^api/v1(?:/|$)#', $path ) && ! preg_match( '#^api/v1/repos/[\w.-]+/[\w.-]+#', $path ) )
|
||||
|| ( preg_match( '#^repos(?:/|$)#', $path ) && ! preg_match( '#^repos/[\w.-]+/[\w.-]+#', $path ) )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
( preg_match( '#^api/v1(?:/|$)#', $path ) && ! preg_match( '#^api/v1/repos/[\w.-]+/[\w.-]+#', $path ) )
|
||||
|| ( preg_match( '#^repos(?:/|$)#', $path ) && ! preg_match( '#^repos/[\w.-]+/[\w.-]+#', $path ) )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$segments = array_values( array_filter( explode( '/', $path ) ) );
|
||||
if ( count( $segments ) >= 2 ) {
|
||||
$repo = $segments[0] . '/' . $segments[1];
|
||||
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $repo ) ) {
|
||||
return $repo;
|
||||
}
|
||||
}
|
||||
$segments = array_values( array_filter( explode( '/', $path ) ) );
|
||||
if ( count( $segments ) >= 2 ) {
|
||||
$repo = $segments[0] . '/' . $segments[1];
|
||||
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $repo ) ) {
|
||||
return $repo;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找下载 URL
|
||||
*
|
||||
* @param array $release Release 数据
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $repo 仓库路径
|
||||
* @return string|null
|
||||
*/
|
||||
private function find_download_url( array $release, string $slug, string $repo ): ?string {
|
||||
if ( ! empty( $release['assets'] ) ) {
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
$name = $asset['name'] ?? '';
|
||||
/**
|
||||
* 查找下载 URL
|
||||
*
|
||||
* @param array $release Release 数据
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $repo 仓库路径
|
||||
* @return string|null
|
||||
*/
|
||||
private function find_download_url( array $release, string $slug, string $repo ): ?string {
|
||||
if ( ! empty( $release['assets'] ) ) {
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
$name = $asset['name'] ?? '';
|
||||
|
||||
if ( preg_match( '/\.zip$/i', $name ) ) {
|
||||
if ( stripos( $name, $slug ) !== false ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( preg_match( '/\.zip$/i', $name ) ) {
|
||||
if ( stripos( $name, $slug ) !== false ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ( $release['assets'] as $asset ) {
|
||||
if ( preg_match( '/\.zip$/i', $asset['name'] ?? '' ) ) {
|
||||
return $asset['browser_download_url'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $release['zipball_url'] ) ) {
|
||||
return $release['zipball_url'];
|
||||
}
|
||||
if ( ! empty( $release['zipball_url'] ) ) {
|
||||
return $release['zipball_url'];
|
||||
}
|
||||
|
||||
$tag = $release['tag_name'] ?? '';
|
||||
if ( ! empty( $tag ) ) {
|
||||
return $this->get_api_base() . '/repos/' . $repo . '/archive/' . $tag . '.zip';
|
||||
}
|
||||
$tag = $release['tag_name'] ?? '';
|
||||
if ( ! empty( $tag ) ) {
|
||||
return $this->get_api_base() . '/repos/' . $repo . '/archive/' . $tag . '.zip';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 API 基础地址
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_api_base(): string {
|
||||
$parts = wp_parse_url( $this->source->api_url );
|
||||
if ( ! empty( $parts['host'] ) ) {
|
||||
$scheme = $parts['scheme'] ?? 'https';
|
||||
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
|
||||
$path = $parts['path'] ?? '';
|
||||
$base_path = '';
|
||||
/**
|
||||
* 获取 API 基础地址
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_api_base(): string {
|
||||
$parts = wp_parse_url( $this->source->api_url );
|
||||
if ( ! empty( $parts['host'] ) ) {
|
||||
$scheme = $parts['scheme'] ?? 'https';
|
||||
$port = isset( $parts['port'] ) ? ':' . $parts['port'] : '';
|
||||
$path = $parts['path'] ?? '';
|
||||
$base_path = '';
|
||||
|
||||
if ( preg_match( '#^(.*?)/api/v1#', $path, $matches ) ) {
|
||||
$base_path = rtrim( $matches[1], '/' );
|
||||
} else {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
if ( ! empty( $repo ) ) {
|
||||
$repo_path = '/' . trim( $repo, '/' );
|
||||
$pos = strpos( $path, $repo_path );
|
||||
if ( false !== $pos ) {
|
||||
$base_path = rtrim( substr( $path, 0, $pos ), '/' );
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( preg_match( '#^(.*?)/api/v1#', $path, $matches ) ) {
|
||||
$base_path = rtrim( $matches[1], '/' );
|
||||
} else {
|
||||
$repo = $this->parse_repo_url( $this->source->api_url );
|
||||
if ( ! empty( $repo ) ) {
|
||||
$repo_path = '/' . trim( $repo, '/' );
|
||||
$pos = strpos( $path, $repo_path );
|
||||
if ( false !== $pos ) {
|
||||
$base_path = rtrim( substr( $path, 0, $pos ), '/' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $scheme . '://' . $parts['host'] . $port . $base_path . '/api/v1';
|
||||
}
|
||||
return $scheme . '://' . $parts['host'] . $port . $base_path . '/api/v1';
|
||||
}
|
||||
|
||||
return self::API_BASE;
|
||||
}
|
||||
return self::API_BASE;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -21,96 +21,96 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class ZipHandler extends AbstractHandler {
|
||||
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return array(
|
||||
'auth' => 'token',
|
||||
'version' => 'zip',
|
||||
'download' => 'direct',
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取能力列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_capabilities(): array {
|
||||
return [
|
||||
'auth' => 'token',
|
||||
'version' => 'zip',
|
||||
'download' => 'direct',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$remote_version = $this->resolve_version();
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||
$remote_version = $this->resolve_version();
|
||||
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::debug( 'ZIP: 无法解析版本号', array( 'url' => $this->source->api_url ) );
|
||||
return null;
|
||||
}
|
||||
if ( empty( $remote_version ) ) {
|
||||
Logger::debug( 'ZIP: 无法解析版本号', [ 'url' => $this->source->api_url ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( ! $this->is_newer_version( $version, $remote_version ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $this->source->api_url;
|
||||
$info->details_url = $this->source->api_url;
|
||||
$info = new UpdateInfo();
|
||||
$info->slug = $slug;
|
||||
$info->version = $remote_version;
|
||||
$info->download_url = $this->source->api_url;
|
||||
$info->details_url = $this->source->api_url;
|
||||
|
||||
return $info;
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$remote_version = $this->resolve_version();
|
||||
/**
|
||||
* 获取项目信息
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_info( string $slug ): ?array {
|
||||
$remote_version = $this->resolve_version();
|
||||
|
||||
if ( empty( $remote_version ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( empty( $remote_version ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'name' => $this->source->name ?: $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $remote_version,
|
||||
'download_url' => $this->source->api_url,
|
||||
'package' => $this->source->api_url,
|
||||
'details_url' => $this->source->api_url,
|
||||
);
|
||||
}
|
||||
return [
|
||||
'name' => $this->source->name ?: $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $remote_version,
|
||||
'download_url' => $this->source->api_url,
|
||||
'package' => $this->source->api_url,
|
||||
'details_url' => $this->source->api_url,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析版本号
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function resolve_version(): string {
|
||||
$metadata = $this->source->metadata ?? array();
|
||||
/**
|
||||
* 解析版本号
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function resolve_version(): string {
|
||||
$metadata = $this->source->metadata ?? [];
|
||||
|
||||
if ( ! empty( $metadata['version'] ) ) {
|
||||
return (string) $metadata['version'];
|
||||
}
|
||||
if ( ! empty( $metadata['version'] ) ) {
|
||||
return (string) $metadata['version'];
|
||||
}
|
||||
|
||||
if ( ! empty( $metadata['new_version'] ) ) {
|
||||
return (string) $metadata['new_version'];
|
||||
}
|
||||
if ( ! empty( $metadata['new_version'] ) ) {
|
||||
return (string) $metadata['new_version'];
|
||||
}
|
||||
|
||||
$path = wp_parse_url( $this->source->api_url, PHP_URL_PATH );
|
||||
if ( empty( $path ) ) {
|
||||
return '';
|
||||
}
|
||||
$path = wp_parse_url( $this->source->api_url, PHP_URL_PATH );
|
||||
if ( empty( $path ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$filename = basename( $path );
|
||||
if ( preg_match( '/(\d+\.\d+\.\d+(?:[-+][\w\.]+)?)/', $filename, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
$filename = basename( $path );
|
||||
if ( preg_match( '/(\d+\.\d+\.\d+(?:[-+][\w\.]+)?)/', $filename, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use WPBridge\Cache\FallbackStrategy;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -23,353 +23,338 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class PluginUpdater {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 源解析器(方案 B)
|
||||
*
|
||||
* @var SourceResolver
|
||||
*/
|
||||
private SourceResolver $source_resolver;
|
||||
/**
|
||||
* 源解析器(方案 B)
|
||||
*
|
||||
* @var SourceResolver
|
||||
*/
|
||||
private SourceResolver $source_resolver;
|
||||
|
||||
/**
|
||||
* 降级策略
|
||||
*
|
||||
* @var FallbackStrategy
|
||||
*/
|
||||
private FallbackStrategy $fallback_strategy;
|
||||
/**
|
||||
* 降级策略
|
||||
*
|
||||
* @var FallbackStrategy
|
||||
*/
|
||||
private FallbackStrategy $fallback_strategy;
|
||||
|
||||
/**
|
||||
* 缓存键前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_PREFIX = 'wpbridge_plugin_update_';
|
||||
/**
|
||||
* 缓存键前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_PREFIX = 'wpbridge_plugin_update_';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->source_resolver = new SourceResolver();
|
||||
$this->fallback_strategy = new FallbackStrategy( $settings );
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->source_resolver = new SourceResolver();
|
||||
$this->fallback_strategy = new FallbackStrategy( $settings );
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 插件更新检查
|
||||
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_updates' ), 10, 1 );
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 插件更新检查
|
||||
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_updates' ], 10, 1 );
|
||||
|
||||
// 插件信息
|
||||
add_filter( 'plugins_api', array( $this, 'plugin_info' ), 10, 3 );
|
||||
// 插件信息
|
||||
add_filter( 'plugins_api', [ $this, 'plugin_info' ], 10, 3 );
|
||||
|
||||
// 下载包过滤
|
||||
add_filter( 'upgrader_pre_download', array( $this, 'filter_download' ), 10, 3 );
|
||||
}
|
||||
// 下载包过滤
|
||||
add_filter( 'upgrader_pre_download', [ $this, 'filter_download' ], 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件更新
|
||||
*
|
||||
* @param object $transient 更新 transient
|
||||
* @return object
|
||||
*/
|
||||
public function check_updates( $transient ) {
|
||||
if ( empty( $transient ) || ! is_object( $transient ) ) {
|
||||
$transient = new \stdClass();
|
||||
}
|
||||
/**
|
||||
* 检查插件更新
|
||||
*
|
||||
* @param object $transient 更新 transient
|
||||
* @return object
|
||||
*/
|
||||
public function check_updates( $transient ) {
|
||||
if ( empty( $transient ) || ! is_object( $transient ) ) {
|
||||
$transient = new \stdClass();
|
||||
}
|
||||
|
||||
if ( ! isset( $transient->response ) ) {
|
||||
$transient->response = array();
|
||||
}
|
||||
if ( ! isset( $transient->response ) ) {
|
||||
$transient->response = [];
|
||||
}
|
||||
|
||||
if ( ! isset( $transient->no_update ) ) {
|
||||
$transient->no_update = array();
|
||||
}
|
||||
if ( ! isset( $transient->no_update ) ) {
|
||||
$transient->no_update = [];
|
||||
}
|
||||
|
||||
// 获取已安装的插件
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
$plugins = get_plugins();
|
||||
// 获取已安装的插件
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
$plugins = get_plugins();
|
||||
|
||||
foreach ( $plugins as $plugin_file => $plugin_data ) {
|
||||
$slug = $this->get_plugin_slug( $plugin_file );
|
||||
foreach ( $plugins as $plugin_file => $plugin_data ) {
|
||||
$slug = $this->get_plugin_slug( $plugin_file );
|
||||
|
||||
$item_key = 'plugin:' . $plugin_file;
|
||||
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'plugin' );
|
||||
$mode = $resolved['mode'];
|
||||
$matching_sources = $resolved['sources'];
|
||||
$allow_wporg_fallback = ! empty( $resolved['has_wporg'] );
|
||||
$item_key = 'plugin:' . $plugin_file;
|
||||
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'plugin' );
|
||||
$mode = $resolved['mode'];
|
||||
$matching_sources = $resolved['sources'];
|
||||
$allow_wporg_fallback = ! empty( $resolved['has_wporg'] );
|
||||
|
||||
if ( $mode === ItemSourceManager::MODE_DISABLED ) {
|
||||
unset( $transient->response[ $plugin_file ] );
|
||||
$transient->no_update[ $plugin_file ] = (object) array(
|
||||
'slug' => $slug,
|
||||
'plugin' => $plugin_file,
|
||||
'new_version' => $plugin_data['Version'],
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ( $mode === ItemSourceManager::MODE_DISABLED ) {
|
||||
unset( $transient->response[ $plugin_file ] );
|
||||
$transient->no_update[ $plugin_file ] = (object) [
|
||||
'slug' => $slug,
|
||||
'plugin' => $plugin_file,
|
||||
'new_version' => $plugin_data['Version'],
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( empty( $matching_sources ) ) {
|
||||
if ( $mode === ItemSourceManager::MODE_CUSTOM ) {
|
||||
unset( $transient->response[ $plugin_file ] );
|
||||
$transient->no_update[ $plugin_file ] = (object) array(
|
||||
'slug' => $slug,
|
||||
'plugin' => $plugin_file,
|
||||
'new_version' => $plugin_data['Version'],
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ( empty( $matching_sources ) ) {
|
||||
if ( $mode === ItemSourceManager::MODE_CUSTOM ) {
|
||||
unset( $transient->response[ $plugin_file ] );
|
||||
$transient->no_update[ $plugin_file ] = (object) [
|
||||
'slug' => $slug,
|
||||
'plugin' => $plugin_file,
|
||||
'new_version' => $plugin_data['Version'],
|
||||
];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$take_over = ( $mode === ItemSourceManager::MODE_CUSTOM ) || ! $allow_wporg_fallback;
|
||||
$take_over = ( $mode === ItemSourceManager::MODE_CUSTOM ) || ! $allow_wporg_fallback;
|
||||
|
||||
if ( $take_over ) {
|
||||
// 接管更新检查,清除默认响应
|
||||
unset( $transient->response[ $plugin_file ] );
|
||||
}
|
||||
if ( $take_over ) {
|
||||
// 接管更新检查,清除默认响应
|
||||
unset( $transient->response[ $plugin_file ] );
|
||||
}
|
||||
|
||||
// 尝试从缓存获取(使用 md5 哈希防止缓存污染)
|
||||
$cache_key = self::CACHE_PREFIX . md5( $slug . get_site_url() );
|
||||
$cached = get_transient( $cache_key );
|
||||
// 尝试从缓存获取(使用 md5 哈希防止缓存污染)
|
||||
$cache_key = self::CACHE_PREFIX . md5( $slug . get_site_url() );
|
||||
$cached = get_transient( $cache_key );
|
||||
|
||||
if ( false !== $cached ) {
|
||||
if ( ! empty( $cached['update'] ) ) {
|
||||
$transient->response[ $plugin_file ] = (object) $cached['update'];
|
||||
unset( $transient->no_update[ $plugin_file ] );
|
||||
} elseif ( $take_over ) {
|
||||
$transient->no_update[ $plugin_file ] = (object) array(
|
||||
'slug' => $slug,
|
||||
'plugin' => $plugin_file,
|
||||
'new_version' => $plugin_data['Version'],
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ( false !== $cached ) {
|
||||
if ( ! empty( $cached['update'] ) ) {
|
||||
$transient->response[ $plugin_file ] = (object) $cached['update'];
|
||||
unset( $transient->no_update[ $plugin_file ] );
|
||||
} else {
|
||||
if ( $take_over ) {
|
||||
$transient->no_update[ $plugin_file ] = (object) [
|
||||
'slug' => $slug,
|
||||
'plugin' => $plugin_file,
|
||||
'new_version' => $plugin_data['Version'],
|
||||
];
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查更新
|
||||
$update_info = $this->check_plugin_update( $slug, $plugin_data['Version'], $matching_sources );
|
||||
// 检查更新
|
||||
$update_info = $this->check_plugin_update( $slug, $plugin_data['Version'], $matching_sources );
|
||||
|
||||
if ( null !== $update_info ) {
|
||||
$update_object = $update_info->to_wp_update_object();
|
||||
$update_object->plugin = $plugin_file;
|
||||
if ( null !== $update_info ) {
|
||||
$update_object = $update_info->to_wp_update_object();
|
||||
$update_object->plugin = $plugin_file;
|
||||
|
||||
$transient->response[ $plugin_file ] = $update_object;
|
||||
unset( $transient->no_update[ $plugin_file ] );
|
||||
$transient->response[ $plugin_file ] = $update_object;
|
||||
unset( $transient->no_update[ $plugin_file ] );
|
||||
|
||||
// 缓存结果
|
||||
set_transient(
|
||||
$cache_key,
|
||||
array(
|
||||
'update' => (array) $update_object,
|
||||
),
|
||||
$this->settings->get_cache_ttl()
|
||||
);
|
||||
// 缓存结果
|
||||
set_transient( $cache_key, [
|
||||
'update' => (array) $update_object,
|
||||
], $this->settings->get_cache_ttl() );
|
||||
|
||||
Logger::info(
|
||||
'插件更新可用',
|
||||
array(
|
||||
'plugin' => $plugin_file,
|
||||
'current' => $plugin_data['Version'],
|
||||
'new' => $update_info->version,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
if ( $take_over ) {
|
||||
$transient->no_update[ $plugin_file ] = (object) array(
|
||||
'slug' => $slug,
|
||||
'plugin' => $plugin_file,
|
||||
'new_version' => $plugin_data['Version'],
|
||||
);
|
||||
}
|
||||
Logger::info( '插件更新可用', [
|
||||
'plugin' => $plugin_file,
|
||||
'current' => $plugin_data['Version'],
|
||||
'new' => $update_info->version,
|
||||
] );
|
||||
} else {
|
||||
if ( $take_over ) {
|
||||
$transient->no_update[ $plugin_file ] = (object) [
|
||||
'slug' => $slug,
|
||||
'plugin' => $plugin_file,
|
||||
'new_version' => $plugin_data['Version'],
|
||||
];
|
||||
}
|
||||
|
||||
// 缓存无更新结果
|
||||
set_transient(
|
||||
$cache_key,
|
||||
array(
|
||||
'update' => null,
|
||||
),
|
||||
$this->settings->get_cache_ttl()
|
||||
);
|
||||
}
|
||||
}
|
||||
// 缓存无更新结果
|
||||
set_transient( $cache_key, [
|
||||
'update' => null,
|
||||
], $this->settings->get_cache_ttl() );
|
||||
}
|
||||
}
|
||||
|
||||
return $transient;
|
||||
}
|
||||
return $transient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个插件更新
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $version 当前版本
|
||||
* @param SourceModel[] $sources 更新源列表
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
private function check_plugin_update( string $slug, string $version, array $sources ): ?UpdateInfo {
|
||||
$cache_key = 'update_info_plugin_' . md5( $slug . get_site_url() );
|
||||
/**
|
||||
* 检查单个插件更新
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @param string $version 当前版本
|
||||
* @param SourceModel[] $sources 更新源列表
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
private function check_plugin_update( string $slug, string $version, array $sources ): ?UpdateInfo {
|
||||
$cache_key = 'update_info_plugin_' . md5( $slug . get_site_url() );
|
||||
|
||||
$result = $this->fallback_strategy->execute_with_fallback(
|
||||
$sources,
|
||||
function ( SourceModel $source ) use ( $slug, $version ) {
|
||||
$handler = $source->get_handler();
|
||||
$result = $this->fallback_strategy->execute_with_fallback(
|
||||
$sources,
|
||||
function( SourceModel $source ) use ( $slug, $version ) {
|
||||
$handler = $source->get_handler();
|
||||
|
||||
if ( null === $handler ) {
|
||||
Logger::warning(
|
||||
'无法获取处理器',
|
||||
array(
|
||||
'source' => $source->id,
|
||||
'type' => $source->type,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if ( null === $handler ) {
|
||||
Logger::warning( '无法获取处理器', [
|
||||
'source' => $source->id,
|
||||
'type' => $source->type,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return $handler->check_update( $slug, $version );
|
||||
} catch ( \Exception $e ) {
|
||||
Logger::error(
|
||||
'检查更新时发生错误',
|
||||
array(
|
||||
'source' => $source->id,
|
||||
'slug' => $slug,
|
||||
'error' => $e->getMessage(),
|
||||
)
|
||||
);
|
||||
throw $e;
|
||||
}
|
||||
},
|
||||
$cache_key
|
||||
);
|
||||
try {
|
||||
return $handler->check_update( $slug, $version );
|
||||
} catch ( \Exception $e ) {
|
||||
Logger::error( '检查更新时发生错误', [
|
||||
'source' => $source->id,
|
||||
'slug' => $slug,
|
||||
'error' => $e->getMessage(),
|
||||
] );
|
||||
throw $e;
|
||||
}
|
||||
},
|
||||
$cache_key
|
||||
);
|
||||
|
||||
return $result instanceof UpdateInfo ? $result : null;
|
||||
}
|
||||
return $result instanceof UpdateInfo ? $result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件信息
|
||||
*
|
||||
* @param false|object|array $result 结果
|
||||
* @param string $action 动作
|
||||
* @param object $args 参数
|
||||
* @return false|object|array
|
||||
*/
|
||||
public function plugin_info( $result, $action, $args ) {
|
||||
if ( 'plugin_information' !== $action ) {
|
||||
return $result;
|
||||
}
|
||||
/**
|
||||
* 获取插件信息
|
||||
*
|
||||
* @param false|object|array $result 结果
|
||||
* @param string $action 动作
|
||||
* @param object $args 参数
|
||||
* @return false|object|array
|
||||
*/
|
||||
public function plugin_info( $result, $action, $args ) {
|
||||
if ( 'plugin_information' !== $action ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$slug = $args->slug ?? '';
|
||||
$slug = $args->slug ?? '';
|
||||
|
||||
if ( empty( $slug ) ) {
|
||||
return $result;
|
||||
}
|
||||
if ( empty( $slug ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$item_key = $this->get_item_key_from_slug( $slug );
|
||||
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'plugin' );
|
||||
$mode = $resolved['mode'];
|
||||
$sources = $resolved['sources'];
|
||||
$item_key = $this->get_item_key_from_slug( $slug );
|
||||
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'plugin' );
|
||||
$mode = $resolved['mode'];
|
||||
$sources = $resolved['sources'];
|
||||
|
||||
if ( $mode === ItemSourceManager::MODE_DISABLED || empty( $sources ) ) {
|
||||
return $result;
|
||||
}
|
||||
if ( $mode === ItemSourceManager::MODE_DISABLED || empty( $sources ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 获取第一个匹配的源
|
||||
$source = reset( $sources );
|
||||
$handler = $source->get_handler();
|
||||
// 获取第一个匹配的源
|
||||
$source = reset( $sources );
|
||||
$handler = $source->get_handler();
|
||||
|
||||
if ( null === $handler ) {
|
||||
return $result;
|
||||
}
|
||||
if ( null === $handler ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$info = $handler->get_info( $slug );
|
||||
$info = $handler->get_info( $slug );
|
||||
|
||||
if ( null === $info ) {
|
||||
return $result;
|
||||
}
|
||||
if ( null === $info ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 转换为 plugins_api 响应格式
|
||||
$update_info = Handlers\UpdateInfo::from_array( $info );
|
||||
return $update_info->to_plugins_api_response( $info['name'] ?? $slug );
|
||||
}
|
||||
// 转换为 plugins_api 响应格式
|
||||
$update_info = Handlers\UpdateInfo::from_array( $info );
|
||||
return $update_info->to_plugins_api_response( $info['name'] ?? $slug );
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 slug 推断项目键
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return string
|
||||
*/
|
||||
private function get_item_key_from_slug( string $slug ): string {
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
/**
|
||||
* 从 slug 推断项目键
|
||||
*
|
||||
* @param string $slug 插件 slug
|
||||
* @return string
|
||||
*/
|
||||
private function get_item_key_from_slug( string $slug ): string {
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
|
||||
$plugins = get_plugins();
|
||||
foreach ( $plugins as $plugin_file => $plugin_data ) {
|
||||
$plugin_slug = $this->get_plugin_slug( $plugin_file );
|
||||
if ( $plugin_slug === $slug ) {
|
||||
return 'plugin:' . $plugin_file;
|
||||
}
|
||||
}
|
||||
$plugins = get_plugins();
|
||||
foreach ( $plugins as $plugin_file => $plugin_data ) {
|
||||
$plugin_slug = $this->get_plugin_slug( $plugin_file );
|
||||
if ( $plugin_slug === $slug ) {
|
||||
return 'plugin:' . $plugin_file;
|
||||
}
|
||||
}
|
||||
|
||||
return 'plugin:' . $slug;
|
||||
}
|
||||
return 'plugin:' . $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤下载
|
||||
*
|
||||
* @param bool $reply 是否已处理
|
||||
* @param string $package 下载包 URL
|
||||
* @param object $upgrader 升级器
|
||||
* @return bool
|
||||
*/
|
||||
public function filter_download( $reply, $package, $upgrader ) {
|
||||
// 目前不做特殊处理,直接返回
|
||||
return $reply;
|
||||
}
|
||||
/**
|
||||
* 过滤下载
|
||||
*
|
||||
* @param bool $reply 是否已处理
|
||||
* @param string $package 下载包 URL
|
||||
* @param object $upgrader 升级器
|
||||
* @return bool
|
||||
*/
|
||||
public function filter_download( $reply, $package, $upgrader ) {
|
||||
// 目前不做特殊处理,直接返回
|
||||
return $reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件 slug
|
||||
*
|
||||
* @param string $plugin_file 插件文件路径
|
||||
* @return string
|
||||
*/
|
||||
private function get_plugin_slug( string $plugin_file ): string {
|
||||
if ( strpos( $plugin_file, '/' ) !== false ) {
|
||||
return dirname( $plugin_file );
|
||||
}
|
||||
return str_replace( '.php', '', $plugin_file );
|
||||
}
|
||||
/**
|
||||
* 获取插件 slug
|
||||
*
|
||||
* @param string $plugin_file 插件文件路径
|
||||
* @return string
|
||||
*/
|
||||
private function get_plugin_slug( string $plugin_file ): string {
|
||||
if ( strpos( $plugin_file, '/' ) !== false ) {
|
||||
return dirname( $plugin_file );
|
||||
}
|
||||
return str_replace( '.php', '', $plugin_file );
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除插件更新缓存
|
||||
*
|
||||
* @param string|null $slug 插件 slug,为空则清除所有
|
||||
*/
|
||||
public function clear_cache( ?string $slug = null ): void {
|
||||
if ( null !== $slug ) {
|
||||
delete_transient( self::CACHE_PREFIX . md5( $slug . get_site_url() ) );
|
||||
} else {
|
||||
global $wpdb;
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%'
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 清除插件更新缓存
|
||||
*
|
||||
* @param string|null $slug 插件 slug,为空则清除所有
|
||||
*/
|
||||
public function clear_cache( ?string $slug = null ): void {
|
||||
if ( null !== $slug ) {
|
||||
delete_transient( self::CACHE_PREFIX . md5( $slug . get_site_url() ) );
|
||||
} else {
|
||||
global $wpdb;
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 清除 WordPress 更新缓存
|
||||
delete_site_transient( 'update_plugins' );
|
||||
}
|
||||
// 清除 WordPress 更新缓存
|
||||
delete_site_transient( 'update_plugins' );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\UpdateSource;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -17,110 +17,110 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class PresetSources {
|
||||
|
||||
/**
|
||||
* 文派开源更新源(默认预置)
|
||||
*/
|
||||
const WENPAI_OPEN = array(
|
||||
'id' => 'wenpai-open',
|
||||
'name' => '文派开源更新源',
|
||||
'type' => SourceType::JSON,
|
||||
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
|
||||
'enabled' => true,
|
||||
'priority' => 10,
|
||||
'is_preset' => true,
|
||||
);
|
||||
/**
|
||||
* 文派开源更新源(默认预置)
|
||||
*/
|
||||
const WENPAI_OPEN = [
|
||||
'id' => 'wenpai-open',
|
||||
'name' => '文派开源更新源',
|
||||
'type' => SourceType::JSON,
|
||||
'api_url' => 'https://updates.wenpai.net/api/v1/plugins/{slug}/info',
|
||||
'enabled' => true,
|
||||
'priority' => 10,
|
||||
'is_preset' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* ArkPress(文派自托管方案)
|
||||
*/
|
||||
const ARKPRESS = array(
|
||||
'id' => 'arkpress',
|
||||
'name' => 'ArkPress',
|
||||
'type' => SourceType::ARKPRESS,
|
||||
'api_url' => '', // 用户自定义
|
||||
'enabled' => false,
|
||||
'priority' => 20,
|
||||
'is_preset' => true,
|
||||
);
|
||||
/**
|
||||
* ArkPress(文派自托管方案)
|
||||
*/
|
||||
const ARKPRESS = [
|
||||
'id' => 'arkpress',
|
||||
'name' => 'ArkPress',
|
||||
'type' => SourceType::ARKPRESS,
|
||||
'api_url' => '', // 用户自定义
|
||||
'enabled' => false,
|
||||
'priority' => 20,
|
||||
'is_preset' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* AspireCloud
|
||||
*/
|
||||
const ASPIRECLOUD = array(
|
||||
'id' => 'aspirecloud',
|
||||
'name' => 'AspireCloud',
|
||||
'type' => SourceType::ASPIRECLOUD,
|
||||
'api_url' => 'https://api.aspirepress.org',
|
||||
'enabled' => false,
|
||||
'priority' => 30,
|
||||
'is_preset' => true,
|
||||
);
|
||||
/**
|
||||
* AspireCloud
|
||||
*/
|
||||
const ASPIRECLOUD = [
|
||||
'id' => 'aspirecloud',
|
||||
'name' => 'AspireCloud',
|
||||
'type' => SourceType::ASPIRECLOUD,
|
||||
'api_url' => 'https://api.aspirepress.org',
|
||||
'enabled' => false,
|
||||
'priority' => 30,
|
||||
'is_preset' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* FAIR Package Manager
|
||||
*/
|
||||
const FAIR = array(
|
||||
'id' => 'fair',
|
||||
'name' => 'FAIR Package Manager',
|
||||
'type' => SourceType::FAIR,
|
||||
'api_url' => 'https://api.fairpm.org',
|
||||
'enabled' => false,
|
||||
'priority' => 40,
|
||||
'is_preset' => true,
|
||||
);
|
||||
/**
|
||||
* FAIR Package Manager
|
||||
*/
|
||||
const FAIR = [
|
||||
'id' => 'fair',
|
||||
'name' => 'FAIR Package Manager',
|
||||
'type' => SourceType::FAIR,
|
||||
'api_url' => 'https://api.fairpm.org',
|
||||
'enabled' => false,
|
||||
'priority' => 40,
|
||||
'is_preset' => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取所有预置源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_all(): array {
|
||||
return array(
|
||||
self::WENPAI_OPEN,
|
||||
// 以下预置源默认不添加,用户可手动启用
|
||||
// self::ARKPRESS,
|
||||
// self::ASPIRECLOUD,
|
||||
// self::FAIR,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取所有预置源
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_all(): array {
|
||||
return [
|
||||
self::WENPAI_OPEN,
|
||||
// 以下预置源默认不添加,用户可手动启用
|
||||
// self::ARKPRESS,
|
||||
// self::ASPIRECLOUD,
|
||||
// self::FAIR,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的预置源模板
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_templates(): array {
|
||||
return array(
|
||||
'arkpress' => self::ARKPRESS,
|
||||
'aspirecloud' => self::ASPIRECLOUD,
|
||||
'fair' => self::FAIR,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取可用的预置源模板
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_templates(): array {
|
||||
return [
|
||||
'arkpress' => self::ARKPRESS,
|
||||
'aspirecloud' => self::ASPIRECLOUD,
|
||||
'fair' => self::FAIR,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取预置源
|
||||
*
|
||||
* @param string $id 预置源 ID
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get_by_id( string $id ): ?array {
|
||||
$all = array(
|
||||
'wenpai-open' => self::WENPAI_OPEN,
|
||||
'arkpress' => self::ARKPRESS,
|
||||
'aspirecloud' => self::ASPIRECLOUD,
|
||||
'fair' => self::FAIR,
|
||||
);
|
||||
/**
|
||||
* 根据 ID 获取预置源
|
||||
*
|
||||
* @param string $id 预置源 ID
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get_by_id( string $id ): ?array {
|
||||
$all = [
|
||||
'wenpai-open' => self::WENPAI_OPEN,
|
||||
'arkpress' => self::ARKPRESS,
|
||||
'aspirecloud' => self::ASPIRECLOUD,
|
||||
'fair' => self::FAIR,
|
||||
];
|
||||
|
||||
return $all[ $id ] ?? null;
|
||||
}
|
||||
return $all[ $id ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是预置源 ID
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_preset_id( string $id ): bool {
|
||||
return in_array( $id, array( 'wenpai-open', 'arkpress', 'aspirecloud', 'fair' ), true );
|
||||
}
|
||||
/**
|
||||
* 检查是否是预置源 ID
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_preset_id( string $id ): bool {
|
||||
return in_array( $id, [ 'wenpai-open', 'arkpress', 'aspirecloud', 'fair' ], true );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20,250 +20,235 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class SourceManager {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 缓存的源模型
|
||||
*
|
||||
* @var array<string, SourceModel>
|
||||
*/
|
||||
private array $source_models = array();
|
||||
/**
|
||||
* 缓存的源模型
|
||||
*
|
||||
* @var array<string, SourceModel>
|
||||
*/
|
||||
private array $source_models = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有源
|
||||
*
|
||||
* @return SourceModel[]
|
||||
*/
|
||||
public function get_all(): array {
|
||||
$sources = $this->settings->get_sources();
|
||||
$models = array();
|
||||
/**
|
||||
* 获取所有源
|
||||
*
|
||||
* @return SourceModel[]
|
||||
*/
|
||||
public function get_all(): array {
|
||||
$sources = $this->settings->get_sources();
|
||||
$models = [];
|
||||
|
||||
foreach ( $sources as $source ) {
|
||||
$model = SourceModel::from_array( $source );
|
||||
$models[ $model->id ] = $model;
|
||||
}
|
||||
foreach ( $sources as $source ) {
|
||||
$model = SourceModel::from_array( $source );
|
||||
$models[ $model->id ] = $model;
|
||||
}
|
||||
|
||||
$this->source_models = $models;
|
||||
return $models;
|
||||
}
|
||||
$this->source_models = $models;
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的源
|
||||
*
|
||||
* @return SourceModel[]
|
||||
*/
|
||||
public function get_enabled(): array {
|
||||
$all = $this->get_all();
|
||||
return array_filter(
|
||||
$all,
|
||||
function ( SourceModel $source ) {
|
||||
return $source->enabled;
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 获取启用的源
|
||||
*
|
||||
* @return SourceModel[]
|
||||
*/
|
||||
public function get_enabled(): array {
|
||||
$all = $this->get_all();
|
||||
return array_filter( $all, function( SourceModel $source ) {
|
||||
return $source->enabled;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* 按优先级排序获取启用的源
|
||||
*
|
||||
* @return SourceModel[]
|
||||
*/
|
||||
public function get_enabled_sorted(): array {
|
||||
$enabled = $this->get_enabled();
|
||||
uasort(
|
||||
$enabled,
|
||||
function ( SourceModel $a, SourceModel $b ) {
|
||||
return $a->priority <=> $b->priority;
|
||||
}
|
||||
);
|
||||
return $enabled;
|
||||
}
|
||||
/**
|
||||
* 按优先级排序获取启用的源
|
||||
*
|
||||
* @return SourceModel[]
|
||||
*/
|
||||
public function get_enabled_sorted(): array {
|
||||
$enabled = $this->get_enabled();
|
||||
uasort( $enabled, function( SourceModel $a, SourceModel $b ) {
|
||||
return $a->priority <=> $b->priority;
|
||||
} );
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @return SourceModel|null
|
||||
*/
|
||||
public function get( string $id ): ?SourceModel {
|
||||
if ( isset( $this->source_models[ $id ] ) ) {
|
||||
return $this->source_models[ $id ];
|
||||
}
|
||||
/**
|
||||
* 获取单个源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @return SourceModel|null
|
||||
*/
|
||||
public function get( string $id ): ?SourceModel {
|
||||
if ( isset( $this->source_models[ $id ] ) ) {
|
||||
return $this->source_models[ $id ];
|
||||
}
|
||||
|
||||
$source = $this->settings->get_source( $id );
|
||||
if ( null === $source ) {
|
||||
return null;
|
||||
}
|
||||
$source = $this->settings->get_source( $id );
|
||||
if ( null === $source ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$model = SourceModel::from_array( $source );
|
||||
$this->source_models[ $id ] = $model;
|
||||
return $model;
|
||||
}
|
||||
$model = SourceModel::from_array( $source );
|
||||
$this->source_models[ $id ] = $model;
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 slug 获取源
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $item_type 项目类型
|
||||
* @return SourceModel[]
|
||||
*/
|
||||
public function get_by_slug( string $slug, string $item_type = 'plugin' ): array {
|
||||
$all = $this->get_enabled_sorted();
|
||||
return array_filter(
|
||||
$all,
|
||||
function ( SourceModel $source ) use ( $slug, $item_type ) {
|
||||
// 空 slug 表示匹配所有
|
||||
if ( empty( $source->slug ) ) {
|
||||
return $source->item_type === $item_type;
|
||||
}
|
||||
return $source->slug === $slug && $source->item_type === $item_type;
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 根据 slug 获取源
|
||||
*
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $item_type 项目类型
|
||||
* @return SourceModel[]
|
||||
*/
|
||||
public function get_by_slug( string $slug, string $item_type = 'plugin' ): array {
|
||||
$all = $this->get_enabled_sorted();
|
||||
return array_filter( $all, function( SourceModel $source ) use ( $slug, $item_type ) {
|
||||
// 空 slug 表示匹配所有
|
||||
if ( empty( $source->slug ) ) {
|
||||
return $source->item_type === $item_type;
|
||||
}
|
||||
return $source->slug === $slug && $source->item_type === $item_type;
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加源
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
* @return bool
|
||||
*/
|
||||
public function add( SourceModel $source ): bool {
|
||||
// 验证
|
||||
$errors = $source->validate();
|
||||
if ( ! empty( $errors ) ) {
|
||||
Logger::error( '添加源失败:验证错误', array( 'errors' => $errors ) );
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 添加源
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
* @return bool
|
||||
*/
|
||||
public function add( SourceModel $source ): bool {
|
||||
// 验证
|
||||
$errors = $source->validate();
|
||||
if ( ! empty( $errors ) ) {
|
||||
Logger::error( '添加源失败:验证错误', [ 'errors' => $errors ] );
|
||||
return false;
|
||||
}
|
||||
|
||||
// 生成 ID
|
||||
if ( empty( $source->id ) ) {
|
||||
$source->id = 'source_' . wp_generate_uuid4();
|
||||
}
|
||||
// 生成 ID
|
||||
if ( empty( $source->id ) ) {
|
||||
$source->id = 'source_' . wp_generate_uuid4();
|
||||
}
|
||||
|
||||
$result = $this->settings->add_source( $source->to_array() );
|
||||
$result = $this->settings->add_source( $source->to_array() );
|
||||
|
||||
if ( $result ) {
|
||||
$this->source_models[ $source->id ] = $source;
|
||||
Logger::info(
|
||||
'添加源成功',
|
||||
array(
|
||||
'id' => $source->id,
|
||||
'name' => $source->name,
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $result ) {
|
||||
$this->source_models[ $source->id ] = $source;
|
||||
Logger::info( '添加源成功', [ 'id' => $source->id, 'name' => $source->name ] );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新源
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
* @return bool
|
||||
*/
|
||||
public function update( SourceModel $source ): bool {
|
||||
// 验证
|
||||
$errors = $source->validate();
|
||||
if ( ! empty( $errors ) ) {
|
||||
Logger::error( '更新源失败:验证错误', array( 'errors' => $errors ) );
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 更新源
|
||||
*
|
||||
* @param SourceModel $source 源模型
|
||||
* @return bool
|
||||
*/
|
||||
public function update( SourceModel $source ): bool {
|
||||
// 验证
|
||||
$errors = $source->validate();
|
||||
if ( ! empty( $errors ) ) {
|
||||
Logger::error( '更新源失败:验证错误', [ 'errors' => $errors ] );
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->settings->update_source( $source->id, $source->to_array() );
|
||||
$result = $this->settings->update_source( $source->id, $source->to_array() );
|
||||
|
||||
if ( $result ) {
|
||||
$this->source_models[ $source->id ] = $source;
|
||||
Logger::info( '更新源成功', array( 'id' => $source->id ) );
|
||||
}
|
||||
if ( $result ) {
|
||||
$this->source_models[ $source->id ] = $source;
|
||||
Logger::info( '更新源成功', [ 'id' => $source->id ] );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $id ): bool {
|
||||
// 不允许删除预置源
|
||||
if ( PresetSources::is_preset_id( $id ) ) {
|
||||
Logger::warning( '尝试删除预置源', array( 'id' => $id ) );
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 删除源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function delete( string $id ): bool {
|
||||
// 不允许删除预置源
|
||||
if ( PresetSources::is_preset_id( $id ) ) {
|
||||
Logger::warning( '尝试删除预置源', [ 'id' => $id ] );
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->settings->delete_source( $id );
|
||||
$result = $this->settings->delete_source( $id );
|
||||
|
||||
if ( $result ) {
|
||||
unset( $this->source_models[ $id ] );
|
||||
Logger::info( '删除源成功', array( 'id' => $id ) );
|
||||
}
|
||||
if ( $result ) {
|
||||
unset( $this->source_models[ $id ] );
|
||||
Logger::info( '删除源成功', [ 'id' => $id ] );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @param bool $enabled 是否启用
|
||||
* @return bool
|
||||
*/
|
||||
public function toggle( string $id, bool $enabled ): bool {
|
||||
$result = $this->settings->toggle_source( $id, $enabled );
|
||||
/**
|
||||
* 启用/禁用源
|
||||
*
|
||||
* @param string $id 源 ID
|
||||
* @param bool $enabled 是否启用
|
||||
* @return bool
|
||||
*/
|
||||
public function toggle( string $id, bool $enabled ): bool {
|
||||
$result = $this->settings->toggle_source( $id, $enabled );
|
||||
|
||||
if ( $result && isset( $this->source_models[ $id ] ) ) {
|
||||
$this->source_models[ $id ]->enabled = $enabled;
|
||||
Logger::info( $enabled ? '启用源' : '禁用源', array( 'id' => $id ) );
|
||||
}
|
||||
if ( $result && isset( $this->source_models[ $id ] ) ) {
|
||||
$this->source_models[ $id ]->enabled = $enabled;
|
||||
Logger::info( $enabled ? '启用源' : '禁用源', [ 'id' => $id ] );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取源统计
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
$all = $this->get_all();
|
||||
$enabled = $this->get_enabled();
|
||||
/**
|
||||
* 获取源统计
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats(): array {
|
||||
$all = $this->get_all();
|
||||
$enabled = $this->get_enabled();
|
||||
|
||||
$by_type = array();
|
||||
foreach ( $all as $source ) {
|
||||
$type = $source->type;
|
||||
if ( ! isset( $by_type[ $type ] ) ) {
|
||||
$by_type[ $type ] = 0;
|
||||
}
|
||||
++$by_type[ $type ];
|
||||
}
|
||||
$by_type = [];
|
||||
foreach ( $all as $source ) {
|
||||
$type = $source->type;
|
||||
if ( ! isset( $by_type[ $type ] ) ) {
|
||||
$by_type[ $type ] = 0;
|
||||
}
|
||||
$by_type[ $type ]++;
|
||||
}
|
||||
|
||||
return array(
|
||||
'total' => count( $all ),
|
||||
'enabled' => count( $enabled ),
|
||||
'by_type' => $by_type,
|
||||
);
|
||||
}
|
||||
return [
|
||||
'total' => count( $all ),
|
||||
'enabled' => count( $enabled ),
|
||||
'by_type' => $by_type,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->source_models = array();
|
||||
$this->settings->clear_cache();
|
||||
}
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public function clear_cache(): void {
|
||||
$this->source_models = [];
|
||||
$this->settings->clear_cache();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20,252 +20,252 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class SourceModel {
|
||||
|
||||
/**
|
||||
* 唯一标识
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $id = '';
|
||||
/**
|
||||
* 唯一标识
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $id = '';
|
||||
|
||||
/**
|
||||
* 源名称
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $name = '';
|
||||
/**
|
||||
* 源名称
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $name = '';
|
||||
|
||||
/**
|
||||
* 源类型(见 SourceType 枚举)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $type = '';
|
||||
/**
|
||||
* 源类型(见 SourceType 枚举)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $type = '';
|
||||
|
||||
/**
|
||||
* API URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $api_url = '';
|
||||
/**
|
||||
* API URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $api_url = '';
|
||||
|
||||
/**
|
||||
* 插件/主题 slug
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $slug = '';
|
||||
/**
|
||||
* 插件/主题 slug
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $slug = '';
|
||||
|
||||
/**
|
||||
* 项目类型:plugin 或 theme
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $item_type = 'plugin';
|
||||
/**
|
||||
* 项目类型:plugin 或 theme
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $item_type = 'plugin';
|
||||
|
||||
/**
|
||||
* 认证令牌
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $auth_token = '';
|
||||
/**
|
||||
* 认证令牌
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $auth_token = '';
|
||||
|
||||
/**
|
||||
* Git 分支(可选)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $branch = '';
|
||||
/**
|
||||
* Git 分支(可选)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $branch = '';
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $enabled = true;
|
||||
/**
|
||||
* 是否启用
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $enabled = true;
|
||||
|
||||
/**
|
||||
* 优先级(数字越小优先级越高)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public int $priority = 50;
|
||||
/**
|
||||
* 优先级(数字越小优先级越高)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public int $priority = 50;
|
||||
|
||||
/**
|
||||
* 是否是预置源
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $is_preset = false;
|
||||
/**
|
||||
* 是否是预置源
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $is_preset = false;
|
||||
|
||||
/**
|
||||
* 是否是内联源(项目专属,通过快速设置创建)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $is_inline = false;
|
||||
/**
|
||||
* 是否是内联源(项目专属,通过快速设置创建)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $is_inline = false;
|
||||
|
||||
/**
|
||||
* 额外元数据
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $metadata = array();
|
||||
/**
|
||||
* 额外元数据
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $metadata = [];
|
||||
|
||||
/**
|
||||
* 从数组创建实例
|
||||
*
|
||||
* @param array $data 数据数组
|
||||
* @return self
|
||||
*/
|
||||
public static function from_array( array $data ): self {
|
||||
$model = new self();
|
||||
/**
|
||||
* 从数组创建实例
|
||||
*
|
||||
* @param array $data 数据数组
|
||||
* @return self
|
||||
*/
|
||||
public static function from_array( array $data ): self {
|
||||
$model = new self();
|
||||
|
||||
$model->id = $data['id'] ?? '';
|
||||
$model->name = $data['name'] ?? '';
|
||||
$model->type = $data['type'] ?? SourceType::JSON;
|
||||
$model->api_url = $data['api_url'] ?? '';
|
||||
$model->slug = $data['slug'] ?? '';
|
||||
$model->item_type = $data['item_type'] ?? 'plugin';
|
||||
$model->auth_token = $data['auth_token'] ?? '';
|
||||
$model->branch = $data['branch'] ?? '';
|
||||
$model->enabled = (bool) ( $data['enabled'] ?? true );
|
||||
$model->priority = (int) ( $data['priority'] ?? 50 );
|
||||
$model->is_preset = (bool) ( $data['is_preset'] ?? false );
|
||||
$model->is_inline = (bool) ( $data['is_inline'] ?? false );
|
||||
$model->metadata = $data['metadata'] ?? array();
|
||||
$model->id = $data['id'] ?? '';
|
||||
$model->name = $data['name'] ?? '';
|
||||
$model->type = $data['type'] ?? SourceType::JSON;
|
||||
$model->api_url = $data['api_url'] ?? '';
|
||||
$model->slug = $data['slug'] ?? '';
|
||||
$model->item_type = $data['item_type'] ?? 'plugin';
|
||||
$model->auth_token = $data['auth_token'] ?? '';
|
||||
$model->branch = $data['branch'] ?? '';
|
||||
$model->enabled = (bool) ( $data['enabled'] ?? true );
|
||||
$model->priority = (int) ( $data['priority'] ?? 50 );
|
||||
$model->is_preset = (bool) ( $data['is_preset'] ?? false );
|
||||
$model->is_inline = (bool) ( $data['is_inline'] ?? false );
|
||||
$model->metadata = $data['metadata'] ?? [];
|
||||
|
||||
return $model;
|
||||
}
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return array(
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'api_url' => $this->api_url,
|
||||
'slug' => $this->slug,
|
||||
'item_type' => $this->item_type,
|
||||
'auth_token' => $this->auth_token,
|
||||
'branch' => $this->branch,
|
||||
'enabled' => $this->enabled,
|
||||
'priority' => $this->priority,
|
||||
'is_preset' => $this->is_preset,
|
||||
'is_inline' => $this->is_inline,
|
||||
'metadata' => $this->metadata,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 转换为数组
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'api_url' => $this->api_url,
|
||||
'slug' => $this->slug,
|
||||
'item_type' => $this->item_type,
|
||||
'auth_token' => $this->auth_token,
|
||||
'branch' => $this->branch,
|
||||
'enabled' => $this->enabled,
|
||||
'priority' => $this->priority,
|
||||
'is_preset' => $this->is_preset,
|
||||
'is_inline' => $this->is_inline,
|
||||
'metadata' => $this->metadata,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证模型
|
||||
*
|
||||
* @return array 错误数组,空数组表示验证通过
|
||||
*/
|
||||
public function validate(): array {
|
||||
$errors = array();
|
||||
/**
|
||||
* 验证模型
|
||||
*
|
||||
* @return array 错误数组,空数组表示验证通过
|
||||
*/
|
||||
public function validate(): array {
|
||||
$errors = [];
|
||||
|
||||
// 验证类型
|
||||
if ( ! SourceType::is_valid( $this->type ) ) {
|
||||
$errors['type'] = __( '无效的源类型', 'wpbridge' );
|
||||
}
|
||||
// 验证类型
|
||||
if ( ! SourceType::is_valid( $this->type ) ) {
|
||||
$errors['type'] = __( '无效的源类型', 'wpbridge' );
|
||||
}
|
||||
|
||||
// 验证 API URL
|
||||
if ( empty( $this->api_url ) ) {
|
||||
$errors['api_url'] = __( 'API URL 不能为空', 'wpbridge' );
|
||||
} elseif ( ! filter_var( $this->api_url, FILTER_VALIDATE_URL ) ) {
|
||||
$errors['api_url'] = __( '无效的 URL 格式', 'wpbridge' );
|
||||
} else {
|
||||
// 检查协议是否为 http/https
|
||||
$scheme = parse_url( $this->api_url, PHP_URL_SCHEME );
|
||||
if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
|
||||
$errors['api_url'] = __( 'URL 必须使用 http 或 https 协议', 'wpbridge' );
|
||||
}
|
||||
}
|
||||
// 验证 API URL
|
||||
if ( empty( $this->api_url ) ) {
|
||||
$errors['api_url'] = __( 'API URL 不能为空', 'wpbridge' );
|
||||
} elseif ( ! filter_var( $this->api_url, FILTER_VALIDATE_URL ) ) {
|
||||
$errors['api_url'] = __( '无效的 URL 格式', 'wpbridge' );
|
||||
} else {
|
||||
// 检查协议是否为 http/https
|
||||
$scheme = parse_url( $this->api_url, PHP_URL_SCHEME );
|
||||
if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
|
||||
$errors['api_url'] = __( 'URL 必须使用 http 或 https 协议', 'wpbridge' );
|
||||
}
|
||||
}
|
||||
|
||||
// 验证项目类型
|
||||
if ( ! in_array( $this->item_type, array( 'plugin', 'theme' ), true ) ) {
|
||||
$errors['item_type'] = __( '项目类型必须是 plugin 或 theme', 'wpbridge' );
|
||||
}
|
||||
// 验证项目类型
|
||||
if ( ! in_array( $this->item_type, [ 'plugin', 'theme' ], true ) ) {
|
||||
$errors['item_type'] = __( '项目类型必须是 plugin 或 theme', 'wpbridge' );
|
||||
}
|
||||
|
||||
// 验证优先级
|
||||
if ( $this->priority < 0 || $this->priority > 100 ) {
|
||||
$errors['priority'] = __( '优先级必须在 0-100 之间', 'wpbridge' );
|
||||
}
|
||||
// 验证优先级
|
||||
if ( $this->priority < 0 || $this->priority > 100 ) {
|
||||
$errors['priority'] = __( '优先级必须在 0-100 之间', 'wpbridge' );
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_valid(): bool {
|
||||
return empty( $this->validate() );
|
||||
}
|
||||
/**
|
||||
* 是否有效
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_valid(): bool {
|
||||
return empty( $this->validate() );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取处理器实例
|
||||
*
|
||||
* @return Handlers\HandlerInterface|null
|
||||
*/
|
||||
public function get_handler(): ?Handlers\HandlerInterface {
|
||||
$handler_class = SourceType::get_handler_class( $this->type );
|
||||
/**
|
||||
* 获取处理器实例
|
||||
*
|
||||
* @return Handlers\HandlerInterface|null
|
||||
*/
|
||||
public function get_handler(): ?Handlers\HandlerInterface {
|
||||
$handler_class = SourceType::get_handler_class( $this->type );
|
||||
|
||||
if ( null === $handler_class || ! class_exists( $handler_class ) ) {
|
||||
return null;
|
||||
}
|
||||
if ( null === $handler_class || ! class_exists( $handler_class ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new $handler_class( $this );
|
||||
}
|
||||
return new $handler_class( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$handler = $this->get_handler();
|
||||
if ( null === $handler ) {
|
||||
return $this->api_url;
|
||||
}
|
||||
return $handler->get_check_url();
|
||||
}
|
||||
/**
|
||||
* 获取检查 URL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_check_url(): string {
|
||||
$handler = $this->get_handler();
|
||||
if ( null === $handler ) {
|
||||
return $this->api_url;
|
||||
}
|
||||
return $handler->get_check_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
$headers = array();
|
||||
/**
|
||||
* 获取请求头
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_headers(): array {
|
||||
$headers = [];
|
||||
|
||||
if ( ! empty( $this->auth_token ) ) {
|
||||
// 解密 auth_token
|
||||
$decrypted_token = Encryption::decrypt( $this->auth_token );
|
||||
if ( ! empty( $this->auth_token ) ) {
|
||||
// 解密 auth_token
|
||||
$decrypted_token = Encryption::decrypt( $this->auth_token );
|
||||
|
||||
// 如果解密失败且数据看起来是加密的,记录错误并返回空
|
||||
if ( empty( $decrypted_token ) ) {
|
||||
if ( Encryption::is_encrypted( $this->auth_token ) ) {
|
||||
Logger::error( 'Token 解密失败', array( 'source' => $this->id ) );
|
||||
return array();
|
||||
}
|
||||
// 可能是未加密的旧数据,直接使用
|
||||
$decrypted_token = $this->auth_token;
|
||||
}
|
||||
// 如果解密失败且数据看起来是加密的,记录错误并返回空
|
||||
if ( empty( $decrypted_token ) ) {
|
||||
if ( Encryption::is_encrypted( $this->auth_token ) ) {
|
||||
Logger::error( 'Token 解密失败', [ 'source' => $this->id ] );
|
||||
return [];
|
||||
}
|
||||
// 可能是未加密的旧数据,直接使用
|
||||
$decrypted_token = $this->auth_token;
|
||||
}
|
||||
|
||||
// 根据类型设置不同的认证头
|
||||
if ( SourceType::is_git_type( $this->type ) ) {
|
||||
$headers['Authorization'] = 'token ' . $decrypted_token;
|
||||
} else {
|
||||
$headers['X-API-Key'] = $decrypted_token;
|
||||
}
|
||||
}
|
||||
// 根据类型设置不同的认证头
|
||||
if ( SourceType::is_git_type( $this->type ) ) {
|
||||
$headers['Authorization'] = 'token ' . $decrypted_token;
|
||||
} else {
|
||||
$headers['X-API-Key'] = $decrypted_token;
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use WPBridge\Core\Logger;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -25,213 +25,213 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class SourceResolver {
|
||||
|
||||
/**
|
||||
* 源注册表
|
||||
*
|
||||
* @var SourceRegistry
|
||||
*/
|
||||
private SourceRegistry $source_registry;
|
||||
/**
|
||||
* 源注册表
|
||||
*
|
||||
* @var SourceRegistry
|
||||
*/
|
||||
private SourceRegistry $source_registry;
|
||||
|
||||
/**
|
||||
* 项目配置管理器
|
||||
*
|
||||
* @var ItemSourceManager
|
||||
*/
|
||||
private ItemSourceManager $item_manager;
|
||||
/**
|
||||
* 项目配置管理器
|
||||
*
|
||||
* @var ItemSourceManager
|
||||
*/
|
||||
private ItemSourceManager $item_manager;
|
||||
|
||||
/**
|
||||
* 默认规则管理器
|
||||
*
|
||||
* @var DefaultsManager
|
||||
*/
|
||||
private DefaultsManager $defaults_manager;
|
||||
/**
|
||||
* 默认规则管理器
|
||||
*
|
||||
* @var DefaultsManager
|
||||
*/
|
||||
private DefaultsManager $defaults_manager;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->source_registry = new SourceRegistry();
|
||||
$this->item_manager = new ItemSourceManager( $this->source_registry );
|
||||
$this->defaults_manager = new DefaultsManager();
|
||||
}
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->source_registry = new SourceRegistry();
|
||||
$this->item_manager = new ItemSourceManager( $this->source_registry );
|
||||
$this->defaults_manager = new DefaultsManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析指定项目的更新源
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $item_type 项目类型
|
||||
* @return array{mode:string,sources:SourceModel[],has_wporg:bool}
|
||||
*/
|
||||
public function resolve( string $item_key, string $slug, string $item_type ): array {
|
||||
$config = $this->item_manager->get( $item_key );
|
||||
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;
|
||||
/**
|
||||
* 解析指定项目的更新源
|
||||
*
|
||||
* @param string $item_key 项目键
|
||||
* @param string $slug 插件/主题 slug
|
||||
* @param string $item_type 项目类型
|
||||
* @return array{mode:string,sources:SourceModel[],has_wporg:bool}
|
||||
*/
|
||||
public function resolve( string $item_key, string $slug, string $item_type ): array {
|
||||
$config = $this->item_manager->get( $item_key );
|
||||
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;
|
||||
|
||||
if ( $mode === ItemSourceManager::MODE_DISABLED ) {
|
||||
return array(
|
||||
'mode' => $mode,
|
||||
'sources' => array(),
|
||||
'has_wporg' => false,
|
||||
);
|
||||
}
|
||||
if ( $mode === ItemSourceManager::MODE_DISABLED ) {
|
||||
return [
|
||||
'mode' => $mode,
|
||||
'sources' => [],
|
||||
'has_wporg' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$sources = $this->item_manager->get_effective_sources( $item_key, $this->defaults_manager );
|
||||
$sources = $this->item_manager->get_effective_sources( $item_key, $this->defaults_manager );
|
||||
|
||||
if ( empty( $sources ) ) {
|
||||
return array(
|
||||
'mode' => $mode,
|
||||
'sources' => array(),
|
||||
'has_wporg' => false,
|
||||
);
|
||||
}
|
||||
if ( empty( $sources ) ) {
|
||||
return [
|
||||
'mode' => $mode,
|
||||
'sources' => [],
|
||||
'has_wporg' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$has_wporg = false;
|
||||
$models = array();
|
||||
foreach ( $sources as $source ) {
|
||||
if ( ( $source['type'] ?? '' ) === SourceRegistry::TYPE_WPORG ) {
|
||||
$has_wporg = true;
|
||||
}
|
||||
$has_wporg = false;
|
||||
$models = [];
|
||||
foreach ( $sources as $source ) {
|
||||
if ( ( $source['type'] ?? '' ) === SourceRegistry::TYPE_WPORG ) {
|
||||
$has_wporg = true;
|
||||
}
|
||||
|
||||
$model = $this->convert_source( $source, $item_type, $slug, $mode === ItemSourceManager::MODE_CUSTOM );
|
||||
if ( null !== $model ) {
|
||||
$models[] = $model;
|
||||
}
|
||||
}
|
||||
$model = $this->convert_source( $source, $item_type, $slug, $mode === ItemSourceManager::MODE_CUSTOM );
|
||||
if ( null !== $model ) {
|
||||
$models[] = $model;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'mode' => $mode,
|
||||
'sources' => $models,
|
||||
'has_wporg' => $has_wporg,
|
||||
);
|
||||
}
|
||||
return [
|
||||
'mode' => $mode,
|
||||
'sources' => $models,
|
||||
'has_wporg' => $has_wporg,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SourceRegistry 记录转换为 SourceModel
|
||||
*
|
||||
* @param array $source 源配置
|
||||
* @param string $item_type 项目类型
|
||||
* @param string $slug 项目 slug
|
||||
* @param bool $force_slug 是否强制绑定到 slug
|
||||
* @return SourceModel|null
|
||||
*/
|
||||
private function convert_source( array $source, string $item_type, string $slug, bool $force_slug ): ?SourceModel {
|
||||
$type = $this->map_type( $source );
|
||||
if ( null === $type ) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 将 SourceRegistry 记录转换为 SourceModel
|
||||
*
|
||||
* @param array $source 源配置
|
||||
* @param string $item_type 项目类型
|
||||
* @param string $slug 项目 slug
|
||||
* @param bool $force_slug 是否强制绑定到 slug
|
||||
* @return SourceModel|null
|
||||
*/
|
||||
private function convert_source( array $source, string $item_type, string $slug, bool $force_slug ): ?SourceModel {
|
||||
$type = $this->map_type( $source );
|
||||
if ( null === $type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$api_url = $source['api_url'] ?? '';
|
||||
if ( empty( $api_url ) ) {
|
||||
Logger::warning( '源缺少 API URL', array( 'source' => $source['source_key'] ?? '' ) );
|
||||
return null;
|
||||
}
|
||||
$api_url = $source['api_url'] ?? '';
|
||||
if ( empty( $api_url ) ) {
|
||||
Logger::warning( '源缺少 API URL', [ 'source' => $source['source_key'] ?? '' ] );
|
||||
return null;
|
||||
}
|
||||
|
||||
$id = $source['source_key'] ?? '';
|
||||
if ( empty( $id ) ) {
|
||||
return null;
|
||||
}
|
||||
$id = $source['source_key'] ?? '';
|
||||
if ( empty( $id ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$model = new SourceModel();
|
||||
$model->id = $id;
|
||||
$model->name = $source['name'] ?? $id;
|
||||
$model->type = $type;
|
||||
$model->api_url = $api_url;
|
||||
$model->item_type = $item_type;
|
||||
$model->slug = $force_slug ? $slug : '';
|
||||
$model->enabled = ! empty( $source['enabled'] );
|
||||
$model->priority = (int) ( $source['priority'] ?? $source['default_priority'] ?? 50 );
|
||||
$model->is_preset = ! empty( $source['is_preset'] );
|
||||
$model->metadata = array(
|
||||
'auth_scheme' => $source['auth_type'] ?? '',
|
||||
'signature_required' => ! empty( $source['signature_required'] ),
|
||||
);
|
||||
$model = new SourceModel();
|
||||
$model->id = $id;
|
||||
$model->name = $source['name'] ?? $id;
|
||||
$model->type = $type;
|
||||
$model->api_url = $api_url;
|
||||
$model->item_type = $item_type;
|
||||
$model->slug = $force_slug ? $slug : '';
|
||||
$model->enabled = ! empty( $source['enabled'] );
|
||||
$model->priority = (int) ( $source['priority'] ?? $source['default_priority'] ?? 50 );
|
||||
$model->is_preset = ! empty( $source['is_preset'] );
|
||||
$model->metadata = [
|
||||
'auth_scheme' => $source['auth_type'] ?? '',
|
||||
'signature_required' => ! empty( $source['signature_required'] ),
|
||||
];
|
||||
|
||||
$secret_ref = $source['auth_secret_ref'] ?? '';
|
||||
if ( ! empty( $secret_ref ) ) {
|
||||
$secret = get_option( 'wpbridge_secret_' . $secret_ref, '' );
|
||||
if ( ! empty( $secret ) ) {
|
||||
$model->auth_token = Encryption::encrypt( $secret );
|
||||
}
|
||||
}
|
||||
$secret_ref = $source['auth_secret_ref'] ?? '';
|
||||
if ( ! empty( $secret_ref ) ) {
|
||||
$secret = get_option( 'wpbridge_secret_' . $secret_ref, '' );
|
||||
if ( ! empty( $secret ) ) {
|
||||
$model->auth_token = Encryption::encrypt( $secret );
|
||||
}
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射源类型
|
||||
*
|
||||
* @param array $source 源配置
|
||||
* @return string|null
|
||||
*/
|
||||
private function map_type( array $source ): ?string {
|
||||
$type = $source['type'] ?? '';
|
||||
/**
|
||||
* 映射源类型
|
||||
*
|
||||
* @param array $source 源配置
|
||||
* @return string|null
|
||||
*/
|
||||
private function map_type( array $source ): ?string {
|
||||
$type = $source['type'] ?? '';
|
||||
|
||||
switch ( $type ) {
|
||||
case SourceRegistry::TYPE_WPORG:
|
||||
return null;
|
||||
switch ( $type ) {
|
||||
case SourceRegistry::TYPE_WPORG:
|
||||
return null;
|
||||
|
||||
case SourceRegistry::TYPE_MIRROR:
|
||||
return SourceType::ARKPRESS;
|
||||
case SourceRegistry::TYPE_MIRROR:
|
||||
return SourceType::ARKPRESS;
|
||||
|
||||
case SourceRegistry::TYPE_FAIR:
|
||||
return SourceType::FAIR;
|
||||
case SourceRegistry::TYPE_FAIR:
|
||||
return SourceType::FAIR;
|
||||
|
||||
case SourceRegistry::TYPE_JSON:
|
||||
return SourceType::JSON;
|
||||
case SourceRegistry::TYPE_JSON:
|
||||
return SourceType::JSON;
|
||||
|
||||
case SourceRegistry::TYPE_ARKPRESS:
|
||||
return SourceType::ARKPRESS;
|
||||
case SourceRegistry::TYPE_ARKPRESS:
|
||||
return SourceType::ARKPRESS;
|
||||
|
||||
case SourceRegistry::TYPE_GIT:
|
||||
return $this->resolve_git_type( $source['api_url'] ?? '' );
|
||||
case SourceRegistry::TYPE_GIT:
|
||||
return $this->resolve_git_type( $source['api_url'] ?? '' );
|
||||
|
||||
case SourceRegistry::TYPE_CUSTOM:
|
||||
return $this->guess_custom_type( $source['api_url'] ?? '' );
|
||||
case SourceRegistry::TYPE_CUSTOM:
|
||||
return $this->guess_custom_type( $source['api_url'] ?? '' );
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Git 类型
|
||||
*
|
||||
* @param string $url 源 URL
|
||||
* @return string
|
||||
*/
|
||||
private function resolve_git_type( string $url ): string {
|
||||
$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
|
||||
/**
|
||||
* 解析 Git 类型
|
||||
*
|
||||
* @param string $url 源 URL
|
||||
* @return string
|
||||
*/
|
||||
private function resolve_git_type( string $url ): string {
|
||||
$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
|
||||
|
||||
if ( strpos( $host, 'github.com' ) !== false ) {
|
||||
return SourceType::GITHUB;
|
||||
}
|
||||
if ( strpos( $host, 'github.com' ) !== false ) {
|
||||
return SourceType::GITHUB;
|
||||
}
|
||||
|
||||
if ( strpos( $host, 'gitlab' ) !== false ) {
|
||||
return SourceType::GITLAB;
|
||||
}
|
||||
if ( strpos( $host, 'gitlab' ) !== false ) {
|
||||
return SourceType::GITLAB;
|
||||
}
|
||||
|
||||
if ( strpos( $host, 'gitee.com' ) !== false ) {
|
||||
return SourceType::GITEE;
|
||||
}
|
||||
if ( strpos( $host, 'gitee.com' ) !== false ) {
|
||||
return SourceType::GITEE;
|
||||
}
|
||||
|
||||
if ( strpos( $host, 'wenpai' ) !== false || strpos( $host, 'feicode' ) !== false ) {
|
||||
return SourceType::WENPAI_GIT;
|
||||
}
|
||||
if ( strpos( $host, 'wenpai' ) !== false || strpos( $host, 'feicode' ) !== false ) {
|
||||
return SourceType::WENPAI_GIT;
|
||||
}
|
||||
|
||||
return SourceType::WENPAI_GIT;
|
||||
}
|
||||
return SourceType::WENPAI_GIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断自定义类型
|
||||
*
|
||||
* @param string $url 源 URL
|
||||
* @return string
|
||||
*/
|
||||
private function guess_custom_type( string $url ): string {
|
||||
if ( preg_match( '/\.zip$/i', $url ) ) {
|
||||
return SourceType::ZIP;
|
||||
}
|
||||
/**
|
||||
* 推断自定义类型
|
||||
*
|
||||
* @param string $url 源 URL
|
||||
* @return string
|
||||
*/
|
||||
private function guess_custom_type( string $url ): string {
|
||||
if ( preg_match( '/\.zip$/i', $url ) ) {
|
||||
return SourceType::ZIP;
|
||||
}
|
||||
|
||||
return SourceType::JSON;
|
||||
}
|
||||
return SourceType::JSON;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace WPBridge\UpdateSource;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -18,188 +18,188 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class SourceType {
|
||||
|
||||
// === 基础类型(用户自定义源)===
|
||||
// === 基础类型(用户自定义源)===
|
||||
|
||||
/**
|
||||
* 标准 JSON API(Plugin Update Checker 格式)
|
||||
*/
|
||||
const JSON = 'json';
|
||||
/**
|
||||
* 标准 JSON API(Plugin Update Checker 格式)
|
||||
*/
|
||||
const JSON = 'json';
|
||||
|
||||
/**
|
||||
* GitHub Releases
|
||||
*/
|
||||
const GITHUB = 'github';
|
||||
/**
|
||||
* GitHub Releases
|
||||
*/
|
||||
const GITHUB = 'github';
|
||||
|
||||
/**
|
||||
* GitLab Releases
|
||||
*/
|
||||
const GITLAB = 'gitlab';
|
||||
/**
|
||||
* GitLab Releases
|
||||
*/
|
||||
const GITLAB = 'gitlab';
|
||||
|
||||
/**
|
||||
* Gitee Releases(国内)
|
||||
*/
|
||||
const GITEE = 'gitee';
|
||||
/**
|
||||
* Gitee Releases(国内)
|
||||
*/
|
||||
const GITEE = 'gitee';
|
||||
|
||||
/**
|
||||
* 菲码源库
|
||||
*/
|
||||
const WENPAI_GIT = 'wenpai_git';
|
||||
/**
|
||||
* 菲码源库
|
||||
*/
|
||||
const WENPAI_GIT = 'wenpai_git';
|
||||
|
||||
/**
|
||||
* 直接 ZIP URL
|
||||
*/
|
||||
const ZIP = 'zip';
|
||||
/**
|
||||
* 直接 ZIP URL
|
||||
*/
|
||||
const ZIP = 'zip';
|
||||
|
||||
// === 自托管服务器类型(预置源使用)===
|
||||
// === 自托管服务器类型(预置源使用)===
|
||||
|
||||
/**
|
||||
* ArkPress(文派自托管,AspireCloud 分叉)
|
||||
*/
|
||||
const ARKPRESS = 'arkpress';
|
||||
/**
|
||||
* ArkPress(文派自托管,AspireCloud 分叉)
|
||||
*/
|
||||
const ARKPRESS = 'arkpress';
|
||||
|
||||
/**
|
||||
* AspireCloud
|
||||
*/
|
||||
const ASPIRECLOUD = 'aspirecloud';
|
||||
/**
|
||||
* AspireCloud
|
||||
*/
|
||||
const ASPIRECLOUD = 'aspirecloud';
|
||||
|
||||
/**
|
||||
* FAIR Package Manager
|
||||
*/
|
||||
const FAIR = 'fair';
|
||||
/**
|
||||
* FAIR Package Manager
|
||||
*/
|
||||
const FAIR = 'fair';
|
||||
|
||||
/**
|
||||
* Plugin Update Checker 服务器
|
||||
*/
|
||||
const PUC = 'puc';
|
||||
/**
|
||||
* Plugin Update Checker 服务器
|
||||
*/
|
||||
const PUC = 'puc';
|
||||
|
||||
/**
|
||||
* WPBridge Server(商业插件桥接服务)
|
||||
*/
|
||||
const BRIDGE_SERVER = 'bridge_server';
|
||||
/**
|
||||
* WPBridge Server(商业插件桥接服务)
|
||||
*/
|
||||
const BRIDGE_SERVER = 'bridge_server';
|
||||
|
||||
// === 类型分组 ===
|
||||
// === 类型分组 ===
|
||||
|
||||
/**
|
||||
* Git 平台类型
|
||||
*/
|
||||
const GIT_TYPES = array(
|
||||
self::GITHUB,
|
||||
self::GITLAB,
|
||||
self::GITEE,
|
||||
self::WENPAI_GIT,
|
||||
);
|
||||
/**
|
||||
* Git 平台类型
|
||||
*/
|
||||
const GIT_TYPES = [
|
||||
self::GITHUB,
|
||||
self::GITLAB,
|
||||
self::GITEE,
|
||||
self::WENPAI_GIT,
|
||||
];
|
||||
|
||||
/**
|
||||
* 自托管服务器类型
|
||||
*/
|
||||
const SERVER_TYPES = array(
|
||||
self::ARKPRESS,
|
||||
self::ASPIRECLOUD,
|
||||
self::FAIR,
|
||||
self::PUC,
|
||||
self::BRIDGE_SERVER,
|
||||
);
|
||||
/**
|
||||
* 自托管服务器类型
|
||||
*/
|
||||
const SERVER_TYPES = [
|
||||
self::ARKPRESS,
|
||||
self::ASPIRECLOUD,
|
||||
self::FAIR,
|
||||
self::PUC,
|
||||
self::BRIDGE_SERVER,
|
||||
];
|
||||
|
||||
/**
|
||||
* 所有类型
|
||||
*/
|
||||
const ALL_TYPES = array(
|
||||
self::JSON,
|
||||
self::GITHUB,
|
||||
self::GITLAB,
|
||||
self::GITEE,
|
||||
self::WENPAI_GIT,
|
||||
self::ZIP,
|
||||
self::ARKPRESS,
|
||||
self::ASPIRECLOUD,
|
||||
self::FAIR,
|
||||
self::PUC,
|
||||
self::BRIDGE_SERVER,
|
||||
);
|
||||
/**
|
||||
* 所有类型
|
||||
*/
|
||||
const ALL_TYPES = [
|
||||
self::JSON,
|
||||
self::GITHUB,
|
||||
self::GITLAB,
|
||||
self::GITEE,
|
||||
self::WENPAI_GIT,
|
||||
self::ZIP,
|
||||
self::ARKPRESS,
|
||||
self::ASPIRECLOUD,
|
||||
self::FAIR,
|
||||
self::PUC,
|
||||
self::BRIDGE_SERVER,
|
||||
];
|
||||
|
||||
/**
|
||||
* 类型标签映射
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_labels(): array {
|
||||
return array(
|
||||
self::JSON => __( 'JSON API', 'wpbridge' ),
|
||||
self::GITHUB => __( 'GitHub', 'wpbridge' ),
|
||||
self::GITLAB => __( 'GitLab', 'wpbridge' ),
|
||||
self::GITEE => __( 'Gitee', 'wpbridge' ),
|
||||
self::WENPAI_GIT => __( '菲码源库', 'wpbridge' ),
|
||||
self::ZIP => __( 'ZIP URL', 'wpbridge' ),
|
||||
self::ARKPRESS => __( 'ArkPress', 'wpbridge' ),
|
||||
self::ASPIRECLOUD => __( 'AspireCloud', 'wpbridge' ),
|
||||
self::FAIR => __( 'FAIR', 'wpbridge' ),
|
||||
self::PUC => __( 'PUC Server', 'wpbridge' ),
|
||||
self::BRIDGE_SERVER => __( 'Bridge Server', 'wpbridge' ),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 类型标签映射
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_labels(): array {
|
||||
return [
|
||||
self::JSON => __( 'JSON API', 'wpbridge' ),
|
||||
self::GITHUB => __( 'GitHub', 'wpbridge' ),
|
||||
self::GITLAB => __( 'GitLab', 'wpbridge' ),
|
||||
self::GITEE => __( 'Gitee', 'wpbridge' ),
|
||||
self::WENPAI_GIT => __( '菲码源库', 'wpbridge' ),
|
||||
self::ZIP => __( 'ZIP URL', 'wpbridge' ),
|
||||
self::ARKPRESS => __( 'ArkPress', 'wpbridge' ),
|
||||
self::ASPIRECLOUD => __( 'AspireCloud', 'wpbridge' ),
|
||||
self::FAIR => __( 'FAIR', 'wpbridge' ),
|
||||
self::PUC => __( 'PUC Server', 'wpbridge' ),
|
||||
self::BRIDGE_SERVER => __( 'Bridge Server', 'wpbridge' ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型标签
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return string
|
||||
*/
|
||||
public static function get_label( string $type ): string {
|
||||
$labels = self::get_labels();
|
||||
return $labels[ $type ] ?? $type;
|
||||
}
|
||||
/**
|
||||
* 获取类型标签
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return string
|
||||
*/
|
||||
public static function get_label( string $type ): string {
|
||||
$labels = self::get_labels();
|
||||
return $labels[ $type ] ?? $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查类型是否有效
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid( string $type ): bool {
|
||||
return in_array( $type, self::ALL_TYPES, true );
|
||||
}
|
||||
/**
|
||||
* 检查类型是否有效
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid( string $type ): bool {
|
||||
return in_array( $type, self::ALL_TYPES, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是 Git 类型
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_git_type( string $type ): bool {
|
||||
return in_array( $type, self::GIT_TYPES, true );
|
||||
}
|
||||
/**
|
||||
* 检查是否是 Git 类型
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_git_type( string $type ): bool {
|
||||
return in_array( $type, self::GIT_TYPES, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是服务器类型
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_server_type( string $type ): bool {
|
||||
return in_array( $type, self::SERVER_TYPES, true );
|
||||
}
|
||||
/**
|
||||
* 检查是否是服务器类型
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_server_type( string $type ): bool {
|
||||
return in_array( $type, self::SERVER_TYPES, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取处理器类名
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return string|null
|
||||
*/
|
||||
public static function get_handler_class( string $type ): ?string {
|
||||
$handlers = array(
|
||||
self::JSON => 'WPBridge\\UpdateSource\\Handlers\\JsonHandler',
|
||||
self::GITHUB => 'WPBridge\\UpdateSource\\Handlers\\GitHubHandler',
|
||||
self::GITLAB => 'WPBridge\\UpdateSource\\Handlers\\GitLabHandler',
|
||||
self::GITEE => 'WPBridge\\UpdateSource\\Handlers\\GiteeHandler',
|
||||
self::WENPAI_GIT => 'WPBridge\\UpdateSource\\Handlers\\WenPaiGitHandler',
|
||||
self::ZIP => 'WPBridge\\UpdateSource\\Handlers\\ZipHandler',
|
||||
self::ARKPRESS => 'WPBridge\\UpdateSource\\Handlers\\ArkPressHandler',
|
||||
self::ASPIRECLOUD => 'WPBridge\\UpdateSource\\Handlers\\AspireCloudHandler',
|
||||
self::FAIR => 'WPBridge\\UpdateSource\\Handlers\\FairHandler',
|
||||
self::PUC => 'WPBridge\\UpdateSource\\Handlers\\PUCHandler',
|
||||
self::BRIDGE_SERVER => 'WPBridge\\UpdateSource\\Handlers\\BridgeServerHandler',
|
||||
);
|
||||
/**
|
||||
* 获取处理器类名
|
||||
*
|
||||
* @param string $type 类型
|
||||
* @return string|null
|
||||
*/
|
||||
public static function get_handler_class( string $type ): ?string {
|
||||
$handlers = [
|
||||
self::JSON => 'WPBridge\\UpdateSource\\Handlers\\JsonHandler',
|
||||
self::GITHUB => 'WPBridge\\UpdateSource\\Handlers\\GitHubHandler',
|
||||
self::GITLAB => 'WPBridge\\UpdateSource\\Handlers\\GitLabHandler',
|
||||
self::GITEE => 'WPBridge\\UpdateSource\\Handlers\\GiteeHandler',
|
||||
self::WENPAI_GIT => 'WPBridge\\UpdateSource\\Handlers\\WenPaiGitHandler',
|
||||
self::ZIP => 'WPBridge\\UpdateSource\\Handlers\\ZipHandler',
|
||||
self::ARKPRESS => 'WPBridge\\UpdateSource\\Handlers\\ArkPressHandler',
|
||||
self::ASPIRECLOUD => 'WPBridge\\UpdateSource\\Handlers\\AspireCloudHandler',
|
||||
self::FAIR => 'WPBridge\\UpdateSource\\Handlers\\FairHandler',
|
||||
self::PUC => 'WPBridge\\UpdateSource\\Handlers\\PUCHandler',
|
||||
self::BRIDGE_SERVER => 'WPBridge\\UpdateSource\\Handlers\\BridgeServerHandler',
|
||||
];
|
||||
|
||||
return $handlers[ $type ] ?? null;
|
||||
}
|
||||
return $handlers[ $type ] ?? null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use WPBridge\Cache\FallbackStrategy;
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -23,310 +23,295 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
*/
|
||||
class ThemeUpdater {
|
||||
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
/**
|
||||
* 设置实例
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* 源解析器(方案 B)
|
||||
*
|
||||
* @var SourceResolver
|
||||
*/
|
||||
private SourceResolver $source_resolver;
|
||||
/**
|
||||
* 源解析器(方案 B)
|
||||
*
|
||||
* @var SourceResolver
|
||||
*/
|
||||
private SourceResolver $source_resolver;
|
||||
|
||||
/**
|
||||
* 降级策略
|
||||
*
|
||||
* @var FallbackStrategy
|
||||
*/
|
||||
private FallbackStrategy $fallback_strategy;
|
||||
/**
|
||||
* 降级策略
|
||||
*
|
||||
* @var FallbackStrategy
|
||||
*/
|
||||
private FallbackStrategy $fallback_strategy;
|
||||
|
||||
/**
|
||||
* 缓存键前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_PREFIX = 'wpbridge_theme_update_';
|
||||
/**
|
||||
* 缓存键前缀
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_PREFIX = 'wpbridge_theme_update_';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->source_resolver = new SourceResolver();
|
||||
$this->fallback_strategy = new FallbackStrategy( $settings );
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param Settings $settings 设置实例
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
$this->source_resolver = new SourceResolver();
|
||||
$this->fallback_strategy = new FallbackStrategy( $settings );
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 主题更新检查
|
||||
add_filter( 'pre_set_site_transient_update_themes', array( $this, 'check_updates' ), 10, 1 );
|
||||
/**
|
||||
* 初始化钩子
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// 主题更新检查
|
||||
add_filter( 'pre_set_site_transient_update_themes', [ $this, 'check_updates' ], 10, 1 );
|
||||
|
||||
// 主题信息
|
||||
add_filter( 'themes_api', array( $this, 'theme_info' ), 10, 3 );
|
||||
}
|
||||
// 主题信息
|
||||
add_filter( 'themes_api', [ $this, 'theme_info' ], 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查主题更新
|
||||
*
|
||||
* @param object $transient 更新 transient
|
||||
* @return object
|
||||
*/
|
||||
public function check_updates( $transient ) {
|
||||
if ( empty( $transient ) || ! is_object( $transient ) ) {
|
||||
$transient = new \stdClass();
|
||||
}
|
||||
/**
|
||||
* 检查主题更新
|
||||
*
|
||||
* @param object $transient 更新 transient
|
||||
* @return object
|
||||
*/
|
||||
public function check_updates( $transient ) {
|
||||
if ( empty( $transient ) || ! is_object( $transient ) ) {
|
||||
$transient = new \stdClass();
|
||||
}
|
||||
|
||||
if ( ! isset( $transient->response ) ) {
|
||||
$transient->response = array();
|
||||
}
|
||||
if ( ! isset( $transient->response ) ) {
|
||||
$transient->response = [];
|
||||
}
|
||||
|
||||
if ( ! isset( $transient->no_update ) ) {
|
||||
$transient->no_update = array();
|
||||
}
|
||||
if ( ! isset( $transient->no_update ) ) {
|
||||
$transient->no_update = [];
|
||||
}
|
||||
|
||||
// 获取已安装的主题
|
||||
$themes = wp_get_themes();
|
||||
// 获取已安装的主题
|
||||
$themes = wp_get_themes();
|
||||
|
||||
foreach ( $themes as $slug => $theme ) {
|
||||
$item_key = 'theme:' . $slug;
|
||||
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'theme' );
|
||||
$mode = $resolved['mode'];
|
||||
$matching_sources = $resolved['sources'];
|
||||
$allow_wporg_fallback = ! empty( $resolved['has_wporg'] );
|
||||
foreach ( $themes as $slug => $theme ) {
|
||||
$item_key = 'theme:' . $slug;
|
||||
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'theme' );
|
||||
$mode = $resolved['mode'];
|
||||
$matching_sources = $resolved['sources'];
|
||||
$allow_wporg_fallback = ! empty( $resolved['has_wporg'] );
|
||||
|
||||
if ( $mode === ItemSourceManager::MODE_DISABLED ) {
|
||||
unset( $transient->response[ $slug ] );
|
||||
$transient->no_update[ $slug ] = array(
|
||||
'theme' => $slug,
|
||||
'new_version' => $theme->get( 'Version' ),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ( $mode === ItemSourceManager::MODE_DISABLED ) {
|
||||
unset( $transient->response[ $slug ] );
|
||||
$transient->no_update[ $slug ] = [
|
||||
'theme' => $slug,
|
||||
'new_version' => $theme->get( 'Version' ),
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( empty( $matching_sources ) ) {
|
||||
if ( $mode === ItemSourceManager::MODE_CUSTOM ) {
|
||||
unset( $transient->response[ $slug ] );
|
||||
$transient->no_update[ $slug ] = array(
|
||||
'theme' => $slug,
|
||||
'new_version' => $theme->get( 'Version' ),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ( empty( $matching_sources ) ) {
|
||||
if ( $mode === ItemSourceManager::MODE_CUSTOM ) {
|
||||
unset( $transient->response[ $slug ] );
|
||||
$transient->no_update[ $slug ] = [
|
||||
'theme' => $slug,
|
||||
'new_version' => $theme->get( 'Version' ),
|
||||
];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$take_over = ( $mode === ItemSourceManager::MODE_CUSTOM ) || ! $allow_wporg_fallback;
|
||||
$take_over = ( $mode === ItemSourceManager::MODE_CUSTOM ) || ! $allow_wporg_fallback;
|
||||
|
||||
if ( $take_over ) {
|
||||
// 接管更新检查,清除默认响应
|
||||
unset( $transient->response[ $slug ] );
|
||||
}
|
||||
if ( $take_over ) {
|
||||
// 接管更新检查,清除默认响应
|
||||
unset( $transient->response[ $slug ] );
|
||||
}
|
||||
|
||||
$version = $theme->get( 'Version' );
|
||||
$version = $theme->get( 'Version' );
|
||||
|
||||
// 尝试从缓存获取(使用 md5 哈希防止缓存污染)
|
||||
$cache_key = self::CACHE_PREFIX . md5( $slug . get_site_url() );
|
||||
$cached = get_transient( $cache_key );
|
||||
// 尝试从缓存获取(使用 md5 哈希防止缓存污染)
|
||||
$cache_key = self::CACHE_PREFIX . md5( $slug . get_site_url() );
|
||||
$cached = get_transient( $cache_key );
|
||||
|
||||
if ( false !== $cached ) {
|
||||
if ( ! empty( $cached['update'] ) ) {
|
||||
$transient->response[ $slug ] = $cached['update'];
|
||||
unset( $transient->no_update[ $slug ] );
|
||||
} elseif ( $take_over ) {
|
||||
$transient->no_update[ $slug ] = array(
|
||||
'theme' => $slug,
|
||||
'new_version' => $version,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ( false !== $cached ) {
|
||||
if ( ! empty( $cached['update'] ) ) {
|
||||
$transient->response[ $slug ] = $cached['update'];
|
||||
unset( $transient->no_update[ $slug ] );
|
||||
} else {
|
||||
if ( $take_over ) {
|
||||
$transient->no_update[ $slug ] = [
|
||||
'theme' => $slug,
|
||||
'new_version' => $version,
|
||||
];
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查更新
|
||||
$update_info = $this->check_theme_update( $slug, $version, $matching_sources );
|
||||
// 检查更新
|
||||
$update_info = $this->check_theme_update( $slug, $version, $matching_sources );
|
||||
|
||||
if ( null !== $update_info ) {
|
||||
$update_data = array(
|
||||
'theme' => $slug,
|
||||
'new_version' => $update_info->version,
|
||||
'url' => $update_info->details_url,
|
||||
'package' => $update_info->download_url,
|
||||
'requires' => $update_info->requires,
|
||||
'requires_php' => $update_info->requires_php,
|
||||
);
|
||||
if ( null !== $update_info ) {
|
||||
$update_data = [
|
||||
'theme' => $slug,
|
||||
'new_version' => $update_info->version,
|
||||
'url' => $update_info->details_url,
|
||||
'package' => $update_info->download_url,
|
||||
'requires' => $update_info->requires,
|
||||
'requires_php' => $update_info->requires_php,
|
||||
];
|
||||
|
||||
$transient->response[ $slug ] = $update_data;
|
||||
unset( $transient->no_update[ $slug ] );
|
||||
$transient->response[ $slug ] = $update_data;
|
||||
unset( $transient->no_update[ $slug ] );
|
||||
|
||||
// 缓存结果
|
||||
set_transient(
|
||||
$cache_key,
|
||||
array(
|
||||
'update' => $update_data,
|
||||
),
|
||||
$this->settings->get_cache_ttl()
|
||||
);
|
||||
// 缓存结果
|
||||
set_transient( $cache_key, [
|
||||
'update' => $update_data,
|
||||
], $this->settings->get_cache_ttl() );
|
||||
|
||||
Logger::info(
|
||||
'主题更新可用',
|
||||
array(
|
||||
'theme' => $slug,
|
||||
'current' => $version,
|
||||
'new' => $update_info->version,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
if ( $take_over ) {
|
||||
$transient->no_update[ $slug ] = array(
|
||||
'theme' => $slug,
|
||||
'new_version' => $version,
|
||||
);
|
||||
}
|
||||
Logger::info( '主题更新可用', [
|
||||
'theme' => $slug,
|
||||
'current' => $version,
|
||||
'new' => $update_info->version,
|
||||
] );
|
||||
} else {
|
||||
if ( $take_over ) {
|
||||
$transient->no_update[ $slug ] = [
|
||||
'theme' => $slug,
|
||||
'new_version' => $version,
|
||||
];
|
||||
}
|
||||
|
||||
// 缓存无更新结果
|
||||
set_transient(
|
||||
$cache_key,
|
||||
array(
|
||||
'update' => null,
|
||||
),
|
||||
$this->settings->get_cache_ttl()
|
||||
);
|
||||
}
|
||||
}
|
||||
// 缓存无更新结果
|
||||
set_transient( $cache_key, [
|
||||
'update' => null,
|
||||
], $this->settings->get_cache_ttl() );
|
||||
}
|
||||
}
|
||||
|
||||
return $transient;
|
||||
}
|
||||
return $transient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个主题更新
|
||||
*
|
||||
* @param string $slug 主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @param SourceModel[] $sources 更新源列表
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
private function check_theme_update( string $slug, string $version, array $sources ): ?UpdateInfo {
|
||||
$cache_key = 'update_info_theme_' . md5( $slug . get_site_url() );
|
||||
/**
|
||||
* 检查单个主题更新
|
||||
*
|
||||
* @param string $slug 主题 slug
|
||||
* @param string $version 当前版本
|
||||
* @param SourceModel[] $sources 更新源列表
|
||||
* @return UpdateInfo|null
|
||||
*/
|
||||
private function check_theme_update( string $slug, string $version, array $sources ): ?UpdateInfo {
|
||||
$cache_key = 'update_info_theme_' . md5( $slug . get_site_url() );
|
||||
|
||||
$result = $this->fallback_strategy->execute_with_fallback(
|
||||
$sources,
|
||||
function ( SourceModel $source ) use ( $slug, $version ) {
|
||||
$handler = $source->get_handler();
|
||||
$result = $this->fallback_strategy->execute_with_fallback(
|
||||
$sources,
|
||||
function( SourceModel $source ) use ( $slug, $version ) {
|
||||
$handler = $source->get_handler();
|
||||
|
||||
if ( null === $handler ) {
|
||||
Logger::warning(
|
||||
'无法获取处理器',
|
||||
array(
|
||||
'source' => $source->id,
|
||||
'type' => $source->type,
|
||||
)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if ( null === $handler ) {
|
||||
Logger::warning( '无法获取处理器', [
|
||||
'source' => $source->id,
|
||||
'type' => $source->type,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return $handler->check_update( $slug, $version );
|
||||
} catch ( \Exception $e ) {
|
||||
Logger::error(
|
||||
'检查主题更新时发生错误',
|
||||
array(
|
||||
'source' => $source->id,
|
||||
'slug' => $slug,
|
||||
'error' => $e->getMessage(),
|
||||
)
|
||||
);
|
||||
throw $e;
|
||||
}
|
||||
},
|
||||
$cache_key
|
||||
);
|
||||
try {
|
||||
return $handler->check_update( $slug, $version );
|
||||
} catch ( \Exception $e ) {
|
||||
Logger::error( '检查主题更新时发生错误', [
|
||||
'source' => $source->id,
|
||||
'slug' => $slug,
|
||||
'error' => $e->getMessage(),
|
||||
] );
|
||||
throw $e;
|
||||
}
|
||||
},
|
||||
$cache_key
|
||||
);
|
||||
|
||||
return $result instanceof UpdateInfo ? $result : null;
|
||||
}
|
||||
return $result instanceof UpdateInfo ? $result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主题信息
|
||||
*
|
||||
* @param false|object|array $result 结果
|
||||
* @param string $action 动作
|
||||
* @param object $args 参数
|
||||
* @return false|object|array
|
||||
*/
|
||||
public function theme_info( $result, $action, $args ) {
|
||||
if ( 'theme_information' !== $action ) {
|
||||
return $result;
|
||||
}
|
||||
/**
|
||||
* 获取主题信息
|
||||
*
|
||||
* @param false|object|array $result 结果
|
||||
* @param string $action 动作
|
||||
* @param object $args 参数
|
||||
* @return false|object|array
|
||||
*/
|
||||
public function theme_info( $result, $action, $args ) {
|
||||
if ( 'theme_information' !== $action ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$slug = $args->slug ?? '';
|
||||
$slug = $args->slug ?? '';
|
||||
|
||||
if ( empty( $slug ) ) {
|
||||
return $result;
|
||||
}
|
||||
if ( empty( $slug ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$item_key = 'theme:' . $slug;
|
||||
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'theme' );
|
||||
$mode = $resolved['mode'];
|
||||
$sources = $resolved['sources'];
|
||||
$item_key = 'theme:' . $slug;
|
||||
$resolved = $this->source_resolver->resolve( $item_key, $slug, 'theme' );
|
||||
$mode = $resolved['mode'];
|
||||
$sources = $resolved['sources'];
|
||||
|
||||
if ( $mode === ItemSourceManager::MODE_DISABLED || empty( $sources ) ) {
|
||||
return $result;
|
||||
}
|
||||
if ( $mode === ItemSourceManager::MODE_DISABLED || empty( $sources ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 获取第一个匹配的源
|
||||
$source = reset( $sources );
|
||||
$handler = $source->get_handler();
|
||||
// 获取第一个匹配的源
|
||||
$source = reset( $sources );
|
||||
$handler = $source->get_handler();
|
||||
|
||||
if ( null === $handler ) {
|
||||
return $result;
|
||||
}
|
||||
if ( null === $handler ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$info = $handler->get_info( $slug );
|
||||
$info = $handler->get_info( $slug );
|
||||
|
||||
if ( null === $info ) {
|
||||
return $result;
|
||||
}
|
||||
if ( null === $info ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 转换为 themes_api 响应格式
|
||||
return (object) array(
|
||||
'name' => $info['name'] ?? $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $info['version'] ?? '',
|
||||
'download_link' => $info['download_url'] ?? $info['package'] ?? '',
|
||||
'requires' => $info['requires'] ?? '',
|
||||
'requires_php' => $info['requires_php'] ?? '',
|
||||
'last_updated' => $info['last_updated'] ?? '',
|
||||
'sections' => $info['sections'] ?? array(),
|
||||
'screenshot_url' => $info['screenshot_url'] ?? '',
|
||||
);
|
||||
}
|
||||
// 转换为 themes_api 响应格式
|
||||
return (object) [
|
||||
'name' => $info['name'] ?? $slug,
|
||||
'slug' => $slug,
|
||||
'version' => $info['version'] ?? '',
|
||||
'download_link' => $info['download_url'] ?? $info['package'] ?? '',
|
||||
'requires' => $info['requires'] ?? '',
|
||||
'requires_php' => $info['requires_php'] ?? '',
|
||||
'last_updated' => $info['last_updated'] ?? '',
|
||||
'sections' => $info['sections'] ?? [],
|
||||
'screenshot_url' => $info['screenshot_url'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除主题更新缓存
|
||||
*
|
||||
* @param string|null $slug 主题 slug,为空则清除所有
|
||||
*/
|
||||
public function clear_cache( ?string $slug = null ): void {
|
||||
if ( null !== $slug ) {
|
||||
delete_transient( self::CACHE_PREFIX . md5( $slug . get_site_url() ) );
|
||||
} else {
|
||||
global $wpdb;
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%'
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 清除主题更新缓存
|
||||
*
|
||||
* @param string|null $slug 主题 slug,为空则清除所有
|
||||
*/
|
||||
public function clear_cache( ?string $slug = null ): void {
|
||||
if ( null !== $slug ) {
|
||||
delete_transient( self::CACHE_PREFIX . md5( $slug . get_site_url() ) );
|
||||
} else {
|
||||
global $wpdb;
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
$wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 清除 WordPress 更新缓存
|
||||
delete_site_transient( 'update_themes' );
|
||||
}
|
||||
// 清除 WordPress 更新缓存
|
||||
delete_site_transient( 'update_themes' );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
77
phpcs.xml
77
phpcs.xml
|
|
@ -1,77 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<ruleset name="WPBridge">
|
||||
<description>文派云桥代码规范</description>
|
||||
|
||||
<file>.</file>
|
||||
<arg name="extensions" value="php"/>
|
||||
<arg name="warning-severity" value="0"/>
|
||||
<exclude-pattern>vendor/*</exclude-pattern>
|
||||
<exclude-pattern>node_modules/*</exclude-pattern>
|
||||
<exclude-pattern>tests/*</exclude-pattern>
|
||||
<exclude-pattern>lib/*</exclude-pattern>
|
||||
|
||||
<rule ref="WordPress-Extra">
|
||||
<!-- 文件命名:项目使用 PSR-4 自动加载,PascalCase 文件名 -->
|
||||
<exclude name="WordPress.Files.FileName.NotHyphenatedLowercase"/>
|
||||
<exclude name="WordPress.Files.FileName.InvalidClassFileName"/>
|
||||
|
||||
<!-- Yoda 条件:风格偏好,非安全问题 -->
|
||||
<exclude name="WordPress.PHP.YodaConditions.NotYoda"/>
|
||||
|
||||
<!-- 短三元运算符:合法 PHP 语法 -->
|
||||
<exclude name="Universal.Operators.DisallowShortTernary.Found"/>
|
||||
|
||||
<!-- 未使用函数参数:WordPress 钩子回调常有未使用参数 -->
|
||||
<exclude name="Generic.CodeAnalysis.UnusedFunctionParameter"/>
|
||||
|
||||
<!-- Nonce 验证:REST API 端点由框架处理认证 -->
|
||||
<exclude name="WordPress.Security.NonceVerification.Missing"/>
|
||||
<exclude name="WordPress.Security.NonceVerification.Recommended"/>
|
||||
|
||||
<!-- 翻译注释:非关键问题 -->
|
||||
<exclude name="WordPress.WP.I18n.MissingTranslatorsComment"/>
|
||||
|
||||
<!-- 保留字参数名:PHP 参数命名 -->
|
||||
<exclude name="Universal.NamingConventions.NoReservedKeywordParameterNames"/>
|
||||
|
||||
<!-- PHP 替代函数:项目中有合理使用场景 -->
|
||||
<exclude name="WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents"/>
|
||||
<exclude name="WordPress.WP.AlternativeFunctions.parse_url_parse_url"/>
|
||||
<exclude name="WordPress.WP.AlternativeFunctions.file_system_operations_file_put_content"/>
|
||||
<exclude name="WordPress.WP.AlternativeFunctions.unlink_unlink"/>
|
||||
<exclude name="WordPress.WP.AlternativeFunctions.file_system_operations_is_writable"/>
|
||||
|
||||
<!-- 全局变量覆盖:模板文件中常见 -->
|
||||
<exclude name="WordPress.WP.GlobalVariablesOverride.Prohibited"/>
|
||||
|
||||
<!-- base64/urlencode/serialize:加密和 API 通信需要 -->
|
||||
<exclude name="WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode"/>
|
||||
<exclude name="WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode"/>
|
||||
<exclude name="WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode"/>
|
||||
<exclude name="WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize"/>
|
||||
|
||||
<!-- 错误抑制:特定场景需要 -->
|
||||
<exclude name="WordPress.PHP.NoSilencedErrors.Discouraged"/>
|
||||
|
||||
<!-- wp_redirect:合理使用 -->
|
||||
<exclude name="WordPress.Security.SafeRedirect.wp_redirect_wp_redirect"/>
|
||||
|
||||
<!-- error_log:Logger 类需要 -->
|
||||
<exclude name="WordPress.PHP.DevelopmentFunctions.error_log_error_log"/>
|
||||
|
||||
<!-- 多对象/注释代码/空 catch/循环中 count/自增 -->
|
||||
<exclude name="Generic.Files.OneObjectStructurePerFile.MultipleFound"/>
|
||||
<exclude name="Squiz.PHP.CommentedOutCode.Found"/>
|
||||
<exclude name="Generic.CodeAnalysis.EmptyStatement.DetectedCatch"/>
|
||||
<exclude name="Squiz.Operators.IncrementDecrementUsage.Found"/>
|
||||
<exclude name="Squiz.PHP.DisallowSizeFunctionsInLoops.Found"/>
|
||||
</rule>
|
||||
|
||||
<!-- 安全规则:降级为 warning(不排除,保留审查能力) -->
|
||||
<rule ref="WordPress.Security.EscapeOutput.ExceptionNotEscaped">
|
||||
<type>warning</type>
|
||||
</rule>
|
||||
<rule ref="WordPress.Security.EscapeOutput.OutputNotEscaped">
|
||||
<type>warning</type>
|
||||
</rule>
|
||||
</ruleset>
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
use WPBridge\UpdateSource\SourceType;
|
||||
|
|
@ -29,116 +29,116 @@ $logs = Logger::get_logs();
|
|||
|
||||
// 健康检查
|
||||
$health_checker = new HealthChecker( $settings_obj );
|
||||
$health_status = array();
|
||||
$health_status = [];
|
||||
foreach ( $sources as $source ) {
|
||||
if ( $source->enabled ) {
|
||||
$cached_status = get_transient( 'wpbridge_health_' . $source->id );
|
||||
// 确保缓存的状态是数组,防止 __PHP_Incomplete_Class 错误
|
||||
if ( $cached_status && is_array( $cached_status ) ) {
|
||||
$health_status[ $source->id ] = $cached_status;
|
||||
}
|
||||
}
|
||||
if ( $source->enabled ) {
|
||||
$cached_status = get_transient( 'wpbridge_health_' . $source->id );
|
||||
// 确保缓存的状态是数组,防止 __PHP_Incomplete_Class 错误
|
||||
if ( $cached_status && is_array( $cached_status ) ) {
|
||||
$health_status[ $source->id ] = $cached_status;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!-- 标题栏 -->
|
||||
<header class="wpbridge-header">
|
||||
<div class="wpbridge-header-left">
|
||||
<span class="dashicons dashicons-networking wpbridge-logo"></span>
|
||||
<h1 class="wpbridge-title">
|
||||
<?php esc_html_e( '云桥', 'wpbridge' ); ?>
|
||||
<span class="wpbridge-version">v<?php echo esc_html( WPBRIDGE_VERSION ); ?></span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="wpbridge-header-left">
|
||||
<span class="dashicons dashicons-networking wpbridge-logo"></span>
|
||||
<h1 class="wpbridge-title">
|
||||
<?php esc_html_e( '云桥', 'wpbridge' ); ?>
|
||||
<span class="wpbridge-version">v<?php echo esc_html( WPBRIDGE_VERSION ); ?></span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-header-right">
|
||||
<a href="https://wenpai.org/plugins/wpbridge" target="_blank" class="wpbridge-header-link">
|
||||
<span class="dashicons dashicons-book"></span>
|
||||
<?php esc_html_e( '文档', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="https://github.com/WenPai-org/wpbridge" target="_blank" class="wpbridge-header-link">
|
||||
<span class="dashicons dashicons-editor-code"></span>
|
||||
<?php esc_html_e( 'GitHub', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="https://wenpai.org/support" target="_blank" class="wpbridge-header-link">
|
||||
<span class="dashicons dashicons-sos"></span>
|
||||
<?php esc_html_e( '支持', 'wpbridge' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="wpbridge-header-right">
|
||||
<a href="https://wenpai.org/plugins/wpbridge" target="_blank" class="wpbridge-header-link">
|
||||
<span class="dashicons dashicons-book"></span>
|
||||
<?php esc_html_e( '文档', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="https://github.com/WenPai-org/wpbridge" target="_blank" class="wpbridge-header-link">
|
||||
<span class="dashicons dashicons-editor-code"></span>
|
||||
<?php esc_html_e( 'GitHub', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="https://wenpai.org/support" target="_blank" class="wpbridge-header-link">
|
||||
<span class="dashicons dashicons-sos"></span>
|
||||
<?php esc_html_e( '支持', 'wpbridge' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="wrap wpbridge-wrap">
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="wpbridge-content">
|
||||
<!-- Tab 卡片 -->
|
||||
<div class="wpbridge-tabs-card">
|
||||
<!-- Tab 导航 -->
|
||||
<nav class="wpbridge-tab-list">
|
||||
<a href="#overview" class="wpbridge-tab wpbridge-tab-active" data-tab="overview">
|
||||
<?php esc_html_e( '概览', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#projects" class="wpbridge-tab" data-tab="projects">
|
||||
<?php esc_html_e( '项目', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#sources" class="wpbridge-tab" data-tab="sources">
|
||||
<?php esc_html_e( '更新源', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#vendors" class="wpbridge-tab" data-tab="vendors">
|
||||
<?php esc_html_e( '供应商', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#diagnostics" class="wpbridge-tab" data-tab="diagnostics">
|
||||
<?php esc_html_e( '诊断', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#settings" class="wpbridge-tab" data-tab="settings">
|
||||
<?php esc_html_e( '设置', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#api" class="wpbridge-tab" data-tab="api">
|
||||
<?php esc_html_e( 'Bridge API', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#logs" class="wpbridge-tab" data-tab="logs">
|
||||
<?php esc_html_e( '日志', 'wpbridge' ); ?>
|
||||
</a>
|
||||
</nav>
|
||||
<!-- 主内容区 -->
|
||||
<div class="wpbridge-content">
|
||||
<!-- Tab 卡片 -->
|
||||
<div class="wpbridge-tabs-card">
|
||||
<!-- Tab 导航 -->
|
||||
<nav class="wpbridge-tab-list">
|
||||
<a href="#overview" class="wpbridge-tab wpbridge-tab-active" data-tab="overview">
|
||||
<?php esc_html_e( '概览', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#projects" class="wpbridge-tab" data-tab="projects">
|
||||
<?php esc_html_e( '项目', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#sources" class="wpbridge-tab" data-tab="sources">
|
||||
<?php esc_html_e( '更新源', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#vendors" class="wpbridge-tab" data-tab="vendors">
|
||||
<?php esc_html_e( '供应商', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#diagnostics" class="wpbridge-tab" data-tab="diagnostics">
|
||||
<?php esc_html_e( '诊断', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#settings" class="wpbridge-tab" data-tab="settings">
|
||||
<?php esc_html_e( '设置', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#api" class="wpbridge-tab" data-tab="api">
|
||||
<?php esc_html_e( 'Bridge API', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<a href="#logs" class="wpbridge-tab" data-tab="logs">
|
||||
<?php esc_html_e( '日志', 'wpbridge' ); ?>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- Tab: 概览 -->
|
||||
<div id="overview" class="wpbridge-tab-pane wpbridge-tab-pane-active">
|
||||
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/overview.php'; ?>
|
||||
</div>
|
||||
<!-- Tab: 概览 -->
|
||||
<div id="overview" class="wpbridge-tab-pane wpbridge-tab-pane-active">
|
||||
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/overview.php'; ?>
|
||||
</div>
|
||||
|
||||
<!-- Tab: 项目 -->
|
||||
<div id="projects" class="wpbridge-tab-pane">
|
||||
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/projects.php'; ?>
|
||||
</div>
|
||||
<!-- Tab: 项目 -->
|
||||
<div id="projects" class="wpbridge-tab-pane">
|
||||
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/projects.php'; ?>
|
||||
</div>
|
||||
|
||||
<!-- Tab: 更新源 -->
|
||||
<div id="sources" class="wpbridge-tab-pane">
|
||||
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/sources.php'; ?>
|
||||
</div>
|
||||
<!-- Tab: 更新源 -->
|
||||
<div id="sources" class="wpbridge-tab-pane">
|
||||
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/sources.php'; ?>
|
||||
</div>
|
||||
|
||||
<!-- Tab: 供应商 -->
|
||||
<div id="vendors" class="wpbridge-tab-pane">
|
||||
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/vendors.php'; ?>
|
||||
</div>
|
||||
<!-- Tab: 供应商 -->
|
||||
<div id="vendors" class="wpbridge-tab-pane">
|
||||
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/vendors.php'; ?>
|
||||
</div>
|
||||
|
||||
<!-- Tab: 诊断 -->
|
||||
<div id="diagnostics" class="wpbridge-tab-pane">
|
||||
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/diagnostics.php'; ?>
|
||||
</div>
|
||||
<!-- Tab: 诊断 -->
|
||||
<div id="diagnostics" class="wpbridge-tab-pane">
|
||||
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/diagnostics.php'; ?>
|
||||
</div>
|
||||
|
||||
<!-- Tab: 设置 -->
|
||||
<div id="settings" class="wpbridge-tab-pane">
|
||||
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/settings.php'; ?>
|
||||
</div>
|
||||
<!-- Tab: 设置 -->
|
||||
<div id="settings" class="wpbridge-tab-pane">
|
||||
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/settings.php'; ?>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Bridge API -->
|
||||
<div id="api" class="wpbridge-tab-pane">
|
||||
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/api.php'; ?>
|
||||
</div>
|
||||
<!-- Tab: Bridge API -->
|
||||
<div id="api" class="wpbridge-tab-pane">
|
||||
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/api.php'; ?>
|
||||
</div>
|
||||
|
||||
<!-- Tab: 日志 -->
|
||||
<div id="logs" class="wpbridge-tab-pane">
|
||||
<?php require WPBRIDGE_PATH . 'templates/admin/tabs/logs.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab: 日志 -->
|
||||
<div id="logs" class="wpbridge-tab-pane">
|
||||
<?php include WPBRIDGE_PATH . 'templates/admin/tabs/logs.php'; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,131 +11,128 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
use WPBridge\Core\DefaultsManager;
|
||||
|
||||
// 获取当前默认规则
|
||||
$defaults = $defaults_manager->get_all();
|
||||
$global_sources = $defaults['global']['source_order'] ?? array();
|
||||
$plugin_sources = $defaults['plugin']['source_order'] ?? array();
|
||||
$theme_sources = $defaults['theme']['source_order'] ?? array();
|
||||
$defaults = $defaults_manager->get_all();
|
||||
$global_sources = $defaults['global']['source_order'] ?? [];
|
||||
$plugin_sources = $defaults['plugin']['source_order'] ?? [];
|
||||
$theme_sources = $defaults['theme']['source_order'] ?? [];
|
||||
?>
|
||||
|
||||
<div class="wpbridge-defaults-config">
|
||||
<div class="wpbridge-section">
|
||||
<h3 class="wpbridge-section-title">
|
||||
<span class="dashicons dashicons-admin-settings"></span>
|
||||
<?php esc_html_e( '默认更新源配置', 'wpbridge' ); ?>
|
||||
</h3>
|
||||
<p class="wpbridge-section-desc">
|
||||
<?php esc_html_e( '配置插件和主题的默认更新源顺序。当项目未单独配置时,将按此顺序查找更新。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="wpbridge-section">
|
||||
<h3 class="wpbridge-section-title">
|
||||
<span class="dashicons dashicons-admin-settings"></span>
|
||||
<?php esc_html_e( '默认更新源配置', 'wpbridge' ); ?>
|
||||
</h3>
|
||||
<p class="wpbridge-section-desc">
|
||||
<?php esc_html_e( '配置插件和主题的默认更新源顺序。当项目未单独配置时,将按此顺序查找更新。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form method="post" id="wpbridge-defaults-form">
|
||||
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||
<input type="hidden" name="wpbridge_action" value="save_defaults">
|
||||
<form method="post" id="wpbridge-defaults-form">
|
||||
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||
<input type="hidden" name="wpbridge_action" value="save_defaults">
|
||||
|
||||
<!-- 全局默认 -->
|
||||
<div class="wpbridge-config-card">
|
||||
<div class="wpbridge-config-card-header">
|
||||
<h4><?php esc_html_e( '全局默认', 'wpbridge' ); ?></h4>
|
||||
<span class="wpbridge-config-card-desc"><?php esc_html_e( '适用于所有未单独配置的项目', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-config-card-body">
|
||||
<div class="wpbridge-source-order" id="wpbridge-global-sources" data-scope="global">
|
||||
<?php
|
||||
foreach ( $all_sources as $source ) :
|
||||
$priority = $global_sources[ $source['source_key'] ] ?? $source['default_priority'];
|
||||
?>
|
||||
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
|
||||
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
|
||||
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
|
||||
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
|
||||
<label class="wpbridge-toggle wpbridge-toggle-sm">
|
||||
<input type="checkbox" name="global_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
|
||||
value="1" <?php checked( isset( $global_sources[ $source['source_key'] ] ) || empty( $global_sources ) ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 全局默认 -->
|
||||
<div class="wpbridge-config-card">
|
||||
<div class="wpbridge-config-card-header">
|
||||
<h4><?php esc_html_e( '全局默认', 'wpbridge' ); ?></h4>
|
||||
<span class="wpbridge-config-card-desc"><?php esc_html_e( '适用于所有未单独配置的项目', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-config-card-body">
|
||||
<div class="wpbridge-source-order" id="wpbridge-global-sources" data-scope="global">
|
||||
<?php foreach ( $all_sources as $source ) :
|
||||
$priority = $global_sources[ $source['source_key'] ] ?? $source['default_priority'];
|
||||
?>
|
||||
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
|
||||
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
|
||||
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
|
||||
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
|
||||
<label class="wpbridge-toggle wpbridge-toggle-sm">
|
||||
<input type="checkbox" name="global_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
|
||||
value="1" <?php checked( isset( $global_sources[ $source['source_key'] ] ) || empty( $global_sources ) ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 插件默认 -->
|
||||
<div class="wpbridge-config-card">
|
||||
<div class="wpbridge-config-card-header">
|
||||
<h4><?php esc_html_e( '插件默认', 'wpbridge' ); ?></h4>
|
||||
<span class="wpbridge-config-card-desc"><?php esc_html_e( '覆盖全局默认,仅适用于插件', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-config-card-body">
|
||||
<label class="wpbridge-checkbox">
|
||||
<input type="checkbox" name="plugin_override" id="plugin_override"
|
||||
<?php checked( ! empty( $plugin_sources ) ); ?>>
|
||||
<span><?php esc_html_e( '为插件使用单独的默认源配置', 'wpbridge' ); ?></span>
|
||||
</label>
|
||||
<div class="wpbridge-source-order wpbridge-source-order-override" id="wpbridge-plugin-sources" data-scope="plugin"
|
||||
style="<?php echo empty( $plugin_sources ) ? 'display:none;' : ''; ?>">
|
||||
<?php
|
||||
foreach ( $all_sources as $source ) :
|
||||
$priority = $plugin_sources[ $source['source_key'] ] ?? $source['default_priority'];
|
||||
?>
|
||||
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
|
||||
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
|
||||
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
|
||||
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
|
||||
<label class="wpbridge-toggle wpbridge-toggle-sm">
|
||||
<input type="checkbox" name="plugin_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
|
||||
value="1" <?php checked( isset( $plugin_sources[ $source['source_key'] ] ) ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 插件默认 -->
|
||||
<div class="wpbridge-config-card">
|
||||
<div class="wpbridge-config-card-header">
|
||||
<h4><?php esc_html_e( '插件默认', 'wpbridge' ); ?></h4>
|
||||
<span class="wpbridge-config-card-desc"><?php esc_html_e( '覆盖全局默认,仅适用于插件', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-config-card-body">
|
||||
<label class="wpbridge-checkbox">
|
||||
<input type="checkbox" name="plugin_override" id="plugin_override"
|
||||
<?php checked( ! empty( $plugin_sources ) ); ?>>
|
||||
<span><?php esc_html_e( '为插件使用单独的默认源配置', 'wpbridge' ); ?></span>
|
||||
</label>
|
||||
<div class="wpbridge-source-order wpbridge-source-order-override" id="wpbridge-plugin-sources" data-scope="plugin"
|
||||
style="<?php echo empty( $plugin_sources ) ? 'display:none;' : ''; ?>">
|
||||
<?php foreach ( $all_sources as $source ) :
|
||||
$priority = $plugin_sources[ $source['source_key'] ] ?? $source['default_priority'];
|
||||
?>
|
||||
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
|
||||
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
|
||||
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
|
||||
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
|
||||
<label class="wpbridge-toggle wpbridge-toggle-sm">
|
||||
<input type="checkbox" name="plugin_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
|
||||
value="1" <?php checked( isset( $plugin_sources[ $source['source_key'] ] ) ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主题默认 -->
|
||||
<div class="wpbridge-config-card">
|
||||
<div class="wpbridge-config-card-header">
|
||||
<h4><?php esc_html_e( '主题默认', 'wpbridge' ); ?></h4>
|
||||
<span class="wpbridge-config-card-desc"><?php esc_html_e( '覆盖全局默认,仅适用于主题', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-config-card-body">
|
||||
<label class="wpbridge-checkbox">
|
||||
<input type="checkbox" name="theme_override" id="theme_override"
|
||||
<?php checked( ! empty( $theme_sources ) ); ?>>
|
||||
<span><?php esc_html_e( '为主题使用单独的默认源配置', 'wpbridge' ); ?></span>
|
||||
</label>
|
||||
<div class="wpbridge-source-order wpbridge-source-order-override" id="wpbridge-theme-sources" data-scope="theme"
|
||||
style="<?php echo empty( $theme_sources ) ? 'display:none;' : ''; ?>">
|
||||
<?php
|
||||
foreach ( $all_sources as $source ) :
|
||||
$priority = $theme_sources[ $source['source_key'] ] ?? $source['default_priority'];
|
||||
?>
|
||||
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
|
||||
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
|
||||
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
|
||||
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
|
||||
<label class="wpbridge-toggle wpbridge-toggle-sm">
|
||||
<input type="checkbox" name="theme_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
|
||||
value="1" <?php checked( isset( $theme_sources[ $source['source_key'] ] ) ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题默认 -->
|
||||
<div class="wpbridge-config-card">
|
||||
<div class="wpbridge-config-card-header">
|
||||
<h4><?php esc_html_e( '主题默认', 'wpbridge' ); ?></h4>
|
||||
<span class="wpbridge-config-card-desc"><?php esc_html_e( '覆盖全局默认,仅适用于主题', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-config-card-body">
|
||||
<label class="wpbridge-checkbox">
|
||||
<input type="checkbox" name="theme_override" id="theme_override"
|
||||
<?php checked( ! empty( $theme_sources ) ); ?>>
|
||||
<span><?php esc_html_e( '为主题使用单独的默认源配置', 'wpbridge' ); ?></span>
|
||||
</label>
|
||||
<div class="wpbridge-source-order wpbridge-source-order-override" id="wpbridge-theme-sources" data-scope="theme"
|
||||
style="<?php echo empty( $theme_sources ) ? 'display:none;' : ''; ?>">
|
||||
<?php foreach ( $all_sources as $source ) :
|
||||
$priority = $theme_sources[ $source['source_key'] ] ?? $source['default_priority'];
|
||||
?>
|
||||
<div class="wpbridge-source-order-item" data-source-key="<?php echo esc_attr( $source['source_key'] ); ?>">
|
||||
<span class="wpbridge-drag-handle dashicons dashicons-menu"></span>
|
||||
<span class="wpbridge-source-order-name"><?php echo esc_html( $source['name'] ); ?></span>
|
||||
<span class="wpbridge-source-order-type wpbridge-badge"><?php echo esc_html( $source['type'] ); ?></span>
|
||||
<label class="wpbridge-toggle wpbridge-toggle-sm">
|
||||
<input type="checkbox" name="theme_sources[<?php echo esc_attr( $source['source_key'] ); ?>]"
|
||||
value="1" <?php checked( isset( $theme_sources[ $source['source_key'] ] ) ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-form-actions">
|
||||
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
|
||||
<span class="dashicons dashicons-saved"></span>
|
||||
<?php esc_html_e( '保存默认规则', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="wpbridge-form-actions">
|
||||
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
|
||||
<span class="dashicons dashicons-saved"></span>
|
||||
<?php esc_html_e( '保存默认规则', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
use WPBridge\Core\ItemSourceManager;
|
||||
|
|
@ -27,259 +27,258 @@ $version_lock = VersionLock::get_instance();
|
|||
|
||||
<!-- 批量操作工具栏 -->
|
||||
<div class="wpbridge-toolbar">
|
||||
<div class="wpbridge-toolbar-left">
|
||||
<label class="wpbridge-checkbox-all">
|
||||
<input type="checkbox" id="wpbridge-select-all-plugins">
|
||||
<span><?php esc_html_e( '全选', 'wpbridge' ); ?></span>
|
||||
</label>
|
||||
<select id="wpbridge-bulk-action-plugins" class="wpbridge-select">
|
||||
<option value=""><?php esc_html_e( '批量操作', 'wpbridge' ); ?></option>
|
||||
<option value="set_source"><?php esc_html_e( '设置更新源', 'wpbridge' ); ?></option>
|
||||
<option value="reset_default"><?php esc_html_e( '重置为默认', 'wpbridge' ); ?></option>
|
||||
<option value="disable"><?php esc_html_e( '禁用更新', 'wpbridge' ); ?></option>
|
||||
</select>
|
||||
<select id="wpbridge-bulk-source-plugins" class="wpbridge-select wpbridge-bulk-source-select" style="display: none;">
|
||||
<option value=""><?php esc_html_e( '-- 选择更新源 --', 'wpbridge' ); ?></option>
|
||||
<?php foreach ( $all_sources as $source ) : ?>
|
||||
<option value="<?php echo esc_attr( $source['source_key'] ); ?>">
|
||||
<?php echo esc_html( $source['name'] ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-apply-bulk-plugins">
|
||||
<?php esc_html_e( '应用', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-toolbar-right">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-refresh-detection" title="<?php esc_attr_e( '重新检测所有插件类型', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '刷新检测', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<input type="search" class="wpbridge-search" id="wpbridge-search-plugins" placeholder="<?php esc_attr_e( '搜索插件...', 'wpbridge' ); ?>" autocomplete="off">
|
||||
</div>
|
||||
<div class="wpbridge-toolbar-left">
|
||||
<label class="wpbridge-checkbox-all">
|
||||
<input type="checkbox" id="wpbridge-select-all-plugins">
|
||||
<span><?php esc_html_e( '全选', 'wpbridge' ); ?></span>
|
||||
</label>
|
||||
<select id="wpbridge-bulk-action-plugins" class="wpbridge-select">
|
||||
<option value=""><?php esc_html_e( '批量操作', 'wpbridge' ); ?></option>
|
||||
<option value="set_source"><?php esc_html_e( '设置更新源', 'wpbridge' ); ?></option>
|
||||
<option value="reset_default"><?php esc_html_e( '重置为默认', 'wpbridge' ); ?></option>
|
||||
<option value="disable"><?php esc_html_e( '禁用更新', 'wpbridge' ); ?></option>
|
||||
</select>
|
||||
<select id="wpbridge-bulk-source-plugins" class="wpbridge-select wpbridge-bulk-source-select" style="display: none;">
|
||||
<option value=""><?php esc_html_e( '-- 选择更新源 --', 'wpbridge' ); ?></option>
|
||||
<?php foreach ( $all_sources as $source ) : ?>
|
||||
<option value="<?php echo esc_attr( $source['source_key'] ); ?>">
|
||||
<?php echo esc_html( $source['name'] ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-apply-bulk-plugins">
|
||||
<?php esc_html_e( '应用', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-toolbar-right">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-refresh-detection" title="<?php esc_attr_e( '重新检测所有插件类型', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '刷新检测', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<input type="search" class="wpbridge-search" id="wpbridge-search-plugins" placeholder="<?php esc_attr_e( '搜索插件...', 'wpbridge' ); ?>" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 插件列表 -->
|
||||
<div class="wpbridge-project-list" id="wpbridge-plugins-list">
|
||||
<?php if ( empty( $installed_plugins ) ) : ?>
|
||||
<div class="wpbridge-empty">
|
||||
<span class="dashicons dashicons-admin-plugins"></span>
|
||||
<h3><?php esc_html_e( '暂无已安装插件', 'wpbridge' ); ?></h3>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<?php
|
||||
foreach ( $installed_plugins as $plugin_file => $plugin_data ) :
|
||||
$item_key = 'plugin:' . $plugin_file;
|
||||
$config = $item_manager->get( $item_key );
|
||||
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;
|
||||
$effective_sources = $item_manager->get_effective_sources( $item_key, $defaults_manager );
|
||||
<?php if ( empty( $installed_plugins ) ) : ?>
|
||||
<div class="wpbridge-empty">
|
||||
<span class="dashicons dashicons-admin-plugins"></span>
|
||||
<h3><?php esc_html_e( '暂无已安装插件', 'wpbridge' ); ?></h3>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<?php foreach ( $installed_plugins as $plugin_file => $plugin_data ) :
|
||||
$item_key = 'plugin:' . $plugin_file;
|
||||
$config = $item_manager->get( $item_key );
|
||||
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;
|
||||
$effective_sources = $item_manager->get_effective_sources( $item_key, $defaults_manager );
|
||||
|
||||
// 获取插件 slug
|
||||
$plugin_slug = dirname( $plugin_file );
|
||||
if ( $plugin_slug === '.' ) {
|
||||
$plugin_slug = basename( $plugin_file, '.php' );
|
||||
}
|
||||
// 获取插件 slug
|
||||
$plugin_slug = dirname( $plugin_file );
|
||||
if ( $plugin_slug === '.' ) {
|
||||
$plugin_slug = basename( $plugin_file, '.php' );
|
||||
}
|
||||
|
||||
// 判断是否激活
|
||||
$is_active = is_plugin_active( $plugin_file );
|
||||
// 判断是否激活
|
||||
$is_active = is_plugin_active( $plugin_file );
|
||||
|
||||
// 获取版本锁定信息
|
||||
$lock_info = $version_lock->get( $item_key );
|
||||
$is_locked = null !== $lock_info;
|
||||
// 获取版本锁定信息
|
||||
$lock_info = $version_lock->get( $item_key );
|
||||
$is_locked = null !== $lock_info;
|
||||
|
||||
// 检测插件类型
|
||||
$type_info = $commercial_detector->detect( $plugin_slug, $plugin_file );
|
||||
$type_label = CommercialDetector::get_type_label( $type_info['type'] );
|
||||
?>
|
||||
<div class="wpbridge-project-item" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-item-type="plugin">
|
||||
<div class="wpbridge-project-checkbox">
|
||||
<input type="checkbox" class="wpbridge-project-select" value="<?php echo esc_attr( $item_key ); ?>">
|
||||
</div>
|
||||
// 检测插件类型
|
||||
$type_info = $commercial_detector->detect( $plugin_slug, $plugin_file );
|
||||
$type_label = CommercialDetector::get_type_label( $type_info['type'] );
|
||||
?>
|
||||
<div class="wpbridge-project-item" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-item-type="plugin">
|
||||
<div class="wpbridge-project-checkbox">
|
||||
<input type="checkbox" class="wpbridge-project-select" value="<?php echo esc_attr( $item_key ); ?>">
|
||||
</div>
|
||||
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-icon wpbridge-project-expand"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
title="<?php esc_attr_e( '展开配置', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-down-alt2"></span>
|
||||
</button>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-icon wpbridge-project-expand"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
title="<?php esc_attr_e( '展开配置', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-down-alt2"></span>
|
||||
</button>
|
||||
|
||||
<div class="wpbridge-project-info">
|
||||
<div class="wpbridge-project-name">
|
||||
<?php echo esc_html( $plugin_data['Name'] ); ?>
|
||||
<?php if ( $is_active ) : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-success"><?php esc_html_e( '已激活', 'wpbridge' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wpbridge-project-meta">
|
||||
<span class="wpbridge-project-version">v<?php echo esc_html( $plugin_data['Version'] ); ?></span>
|
||||
<span class="wpbridge-project-slug"><?php echo esc_html( $plugin_slug ); ?></span>
|
||||
<a href="#" class="wpbridge-view-changelog"
|
||||
data-slug="<?php echo esc_attr( $plugin_slug ); ?>"
|
||||
data-type="plugin"
|
||||
data-source-type="wporg"
|
||||
title="<?php esc_attr_e( '查看更新日志', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-list-view"></span>
|
||||
</a>
|
||||
<?php if ( ! empty( $plugin_data['Author'] ) ) : ?>
|
||||
<span class="wpbridge-project-author"><?php echo esc_html( $plugin_data['Author'] ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-project-info">
|
||||
<div class="wpbridge-project-name">
|
||||
<?php echo esc_html( $plugin_data['Name'] ); ?>
|
||||
<?php if ( $is_active ) : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-success"><?php esc_html_e( '已激活', 'wpbridge' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wpbridge-project-meta">
|
||||
<span class="wpbridge-project-version">v<?php echo esc_html( $plugin_data['Version'] ); ?></span>
|
||||
<span class="wpbridge-project-slug"><?php echo esc_html( $plugin_slug ); ?></span>
|
||||
<a href="#" class="wpbridge-view-changelog"
|
||||
data-slug="<?php echo esc_attr( $plugin_slug ); ?>"
|
||||
data-type="plugin"
|
||||
data-source-type="wporg"
|
||||
title="<?php esc_attr_e( '查看更新日志', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-list-view"></span>
|
||||
</a>
|
||||
<?php if ( ! empty( $plugin_data['Author'] ) ) : ?>
|
||||
<span class="wpbridge-project-author"><?php echo esc_html( $plugin_data['Author'] ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-project-status">
|
||||
<!-- 插件类型徽章 -->
|
||||
<span class="wpbridge-status-badge wpbridge-status-type-<?php echo esc_attr( $type_info['type'] ); ?>"
|
||||
data-plugin-slug="<?php echo esc_attr( $plugin_slug ); ?>"
|
||||
data-source="<?php echo esc_attr( $type_info['source'] ); ?>"
|
||||
title="<?php echo esc_attr( $type_info['source'] === 'manual' ? __( '手动标记', 'wpbridge' ) : __( '自动检测', 'wpbridge' ) ); ?>">
|
||||
<span class="dashicons <?php echo esc_attr( $type_label['icon'] ); ?>"></span>
|
||||
<?php echo esc_html( $type_label['label'] ); ?>
|
||||
</span>
|
||||
<?php if ( $is_locked ) : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-locked wpbridge-version-lock-badge"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
title="<?php echo esc_attr( VersionLock::get_type_label( $lock_info['type'] ) ); ?>">
|
||||
<span class="dashicons dashicons-lock"></span>
|
||||
<?php esc_html_e( '已锁定', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-disabled">
|
||||
<span class="dashicons dashicons-dismiss"></span>
|
||||
<?php esc_html_e( '已禁用', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php elseif ( $mode === ItemSourceManager::MODE_CUSTOM ) : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-custom">
|
||||
<span class="dashicons dashicons-admin-links"></span>
|
||||
<?php esc_html_e( '自定义', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-default">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
<?php esc_html_e( '默认', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wpbridge-project-status">
|
||||
<!-- 插件类型徽章 -->
|
||||
<span class="wpbridge-status-badge wpbridge-status-type-<?php echo esc_attr( $type_info['type'] ); ?>"
|
||||
data-plugin-slug="<?php echo esc_attr( $plugin_slug ); ?>"
|
||||
data-source="<?php echo esc_attr( $type_info['source'] ); ?>"
|
||||
title="<?php echo esc_attr( $type_info['source'] === 'manual' ? __( '手动标记', 'wpbridge' ) : __( '自动检测', 'wpbridge' ) ); ?>">
|
||||
<span class="dashicons <?php echo esc_attr( $type_label['icon'] ); ?>"></span>
|
||||
<?php echo esc_html( $type_label['label'] ); ?>
|
||||
</span>
|
||||
<?php if ( $is_locked ) : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-locked wpbridge-version-lock-badge"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
title="<?php echo esc_attr( VersionLock::get_type_label( $lock_info['type'] ) ); ?>">
|
||||
<span class="dashicons dashicons-lock"></span>
|
||||
<?php esc_html_e( '已锁定', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-disabled">
|
||||
<span class="dashicons dashicons-dismiss"></span>
|
||||
<?php esc_html_e( '已禁用', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php elseif ( $mode === ItemSourceManager::MODE_CUSTOM ) : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-custom">
|
||||
<span class="dashicons dashicons-admin-links"></span>
|
||||
<?php esc_html_e( '自定义', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-default">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
<?php esc_html_e( '默认', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 内联配置面板(默认折叠) -->
|
||||
<div class="wpbridge-project-config-panel" data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
|
||||
<!-- P2: 插件类型手动标记 -->
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '插件类型', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<select class="wpbridge-form-input wpbridge-plugin-type-select"
|
||||
data-plugin-slug="<?php echo esc_attr( $plugin_slug ); ?>">
|
||||
<option value="<?php echo esc_attr( CommercialDetector::TYPE_UNKNOWN ); ?>"
|
||||
<?php selected( $type_info['type'], CommercialDetector::TYPE_UNKNOWN ); ?>>
|
||||
<?php esc_html_e( '自动检测', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="<?php echo esc_attr( CommercialDetector::TYPE_FREE ); ?>"
|
||||
<?php selected( $type_info['type'], CommercialDetector::TYPE_FREE ); ?>>
|
||||
<?php esc_html_e( '免费插件', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="<?php echo esc_attr( CommercialDetector::TYPE_COMMERCIAL ); ?>"
|
||||
<?php selected( $type_info['type'], CommercialDetector::TYPE_COMMERCIAL ); ?>>
|
||||
<?php esc_html_e( '商业插件', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="<?php echo esc_attr( CommercialDetector::TYPE_PRIVATE ); ?>"
|
||||
<?php selected( $type_info['type'], CommercialDetector::TYPE_PRIVATE ); ?>>
|
||||
<?php esc_html_e( '私有插件', 'wpbridge' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="wpbridge-form-help">
|
||||
<?php if ( $type_info['source'] === 'manual' ) : ?>
|
||||
<?php esc_html_e( '当前为手动标记', 'wpbridge' ); ?>
|
||||
<?php else : ?>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: detection source */
|
||||
esc_html__( '自动检测结果(来源:%s)', 'wpbridge' ),
|
||||
esc_html( $type_info['source'] )
|
||||
);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<input type="url" class="wpbridge-form-input wpbridge-inline-url"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
placeholder="https://github.com/user/repo 或 https://example.com/update.json"
|
||||
autocomplete="off">
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '粘贴更新源地址,系统会自动识别类型', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<input type="password" class="wpbridge-form-input wpbridge-inline-token"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
placeholder="<?php esc_attr_e( '可选,用于私有仓库', 'wpbridge' ); ?>"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
<!-- 版本锁定 -->
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '版本锁定', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<div class="wpbridge-version-lock-controls" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-current-version="<?php echo esc_attr( $plugin_data['Version'] ); ?>">
|
||||
<?php if ( $is_locked ) : ?>
|
||||
<span class="wpbridge-lock-status">
|
||||
<span class="dashicons dashicons-lock"></span>
|
||||
<?php echo esc_html( VersionLock::get_type_label( $lock_info['type'] ) ); ?>
|
||||
<?php if ( ! empty( $lock_info['version'] ) ) : ?>
|
||||
(v<?php echo esc_html( $lock_info['version'] ); ?>)
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-unlock-version"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-unlock"></span>
|
||||
<?php esc_html_e( '解锁', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<select class="wpbridge-form-input wpbridge-lock-type-select" style="max-width: 150px;">
|
||||
<option value=""><?php esc_html_e( '不锁定', 'wpbridge' ); ?></option>
|
||||
<option value="current"><?php esc_html_e( '锁定当前版本', 'wpbridge' ); ?></option>
|
||||
</select>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-lock-version"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
|
||||
<span class="dashicons dashicons-lock"></span>
|
||||
<?php esc_html_e( '锁定', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '锁定后将阻止此插件的自动更新', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-config-actions">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-btn-sm wpbridge-save-inline"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-saved"></span>
|
||||
<?php esc_html_e( '保存', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php if ( $mode !== ItemSourceManager::MODE_DEFAULT ) : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-reset-default"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<?php esc_html_e( '重置为默认', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-enable-update"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '启用更新', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-disable-update"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-dismiss"></span>
|
||||
<?php esc_html_e( '禁用更新', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<!-- 内联配置面板(默认折叠) -->
|
||||
<div class="wpbridge-project-config-panel" data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
|
||||
<!-- P2: 插件类型手动标记 -->
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '插件类型', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<select class="wpbridge-form-input wpbridge-plugin-type-select"
|
||||
data-plugin-slug="<?php echo esc_attr( $plugin_slug ); ?>">
|
||||
<option value="<?php echo esc_attr( CommercialDetector::TYPE_UNKNOWN ); ?>"
|
||||
<?php selected( $type_info['type'], CommercialDetector::TYPE_UNKNOWN ); ?>>
|
||||
<?php esc_html_e( '自动检测', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="<?php echo esc_attr( CommercialDetector::TYPE_FREE ); ?>"
|
||||
<?php selected( $type_info['type'], CommercialDetector::TYPE_FREE ); ?>>
|
||||
<?php esc_html_e( '免费插件', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="<?php echo esc_attr( CommercialDetector::TYPE_COMMERCIAL ); ?>"
|
||||
<?php selected( $type_info['type'], CommercialDetector::TYPE_COMMERCIAL ); ?>>
|
||||
<?php esc_html_e( '商业插件', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="<?php echo esc_attr( CommercialDetector::TYPE_PRIVATE ); ?>"
|
||||
<?php selected( $type_info['type'], CommercialDetector::TYPE_PRIVATE ); ?>>
|
||||
<?php esc_html_e( '私有插件', 'wpbridge' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="wpbridge-form-help">
|
||||
<?php if ( $type_info['source'] === 'manual' ) : ?>
|
||||
<?php esc_html_e( '当前为手动标记', 'wpbridge' ); ?>
|
||||
<?php else : ?>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: detection source */
|
||||
esc_html__( '自动检测结果(来源:%s)', 'wpbridge' ),
|
||||
esc_html( $type_info['source'] )
|
||||
);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<input type="url" class="wpbridge-form-input wpbridge-inline-url"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
placeholder="https://github.com/user/repo 或 https://example.com/update.json"
|
||||
autocomplete="off">
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '粘贴更新源地址,系统会自动识别类型', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<input type="password" class="wpbridge-form-input wpbridge-inline-token"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
placeholder="<?php esc_attr_e( '可选,用于私有仓库', 'wpbridge' ); ?>"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
<!-- 版本锁定 -->
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '版本锁定', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<div class="wpbridge-version-lock-controls" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-current-version="<?php echo esc_attr( $plugin_data['Version'] ); ?>">
|
||||
<?php if ( $is_locked ) : ?>
|
||||
<span class="wpbridge-lock-status">
|
||||
<span class="dashicons dashicons-lock"></span>
|
||||
<?php echo esc_html( VersionLock::get_type_label( $lock_info['type'] ) ); ?>
|
||||
<?php if ( ! empty( $lock_info['version'] ) ) : ?>
|
||||
(v<?php echo esc_html( $lock_info['version'] ); ?>)
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-unlock-version"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-unlock"></span>
|
||||
<?php esc_html_e( '解锁', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<select class="wpbridge-form-input wpbridge-lock-type-select" style="max-width: 150px;">
|
||||
<option value=""><?php esc_html_e( '不锁定', 'wpbridge' ); ?></option>
|
||||
<option value="current"><?php esc_html_e( '锁定当前版本', 'wpbridge' ); ?></option>
|
||||
</select>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-lock-version"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
|
||||
<span class="dashicons dashicons-lock"></span>
|
||||
<?php esc_html_e( '锁定', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '锁定后将阻止此插件的自动更新', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-config-actions">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-btn-sm wpbridge-save-inline"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-saved"></span>
|
||||
<?php esc_html_e( '保存', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php if ( $mode !== ItemSourceManager::MODE_DEFAULT ) : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-reset-default"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<?php esc_html_e( '重置为默认', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-enable-update"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '启用更新', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-disable-update"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-dismiss"></span>
|
||||
<?php esc_html_e( '禁用更新', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,178 +12,177 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
use WPBridge\Core\ItemSourceManager;
|
||||
|
||||
// 获取当前主题
|
||||
$current_theme = wp_get_theme();
|
||||
$current_theme = wp_get_theme();
|
||||
$current_theme_slug = $current_theme->get_stylesheet();
|
||||
?>
|
||||
|
||||
<!-- 批量操作工具栏 -->
|
||||
<div class="wpbridge-toolbar">
|
||||
<div class="wpbridge-toolbar-left">
|
||||
<label class="wpbridge-checkbox-all">
|
||||
<input type="checkbox" id="wpbridge-select-all-themes">
|
||||
<span><?php esc_html_e( '全选', 'wpbridge' ); ?></span>
|
||||
</label>
|
||||
<select id="wpbridge-bulk-action-themes" class="wpbridge-select">
|
||||
<option value=""><?php esc_html_e( '批量操作', 'wpbridge' ); ?></option>
|
||||
<option value="set_source"><?php esc_html_e( '设置更新源', 'wpbridge' ); ?></option>
|
||||
<option value="reset_default"><?php esc_html_e( '重置为默认', 'wpbridge' ); ?></option>
|
||||
<option value="disable"><?php esc_html_e( '禁用更新', 'wpbridge' ); ?></option>
|
||||
</select>
|
||||
<select id="wpbridge-bulk-source-themes" class="wpbridge-select wpbridge-bulk-source-select" style="display: none;">
|
||||
<option value=""><?php esc_html_e( '-- 选择更新源 --', 'wpbridge' ); ?></option>
|
||||
<?php foreach ( $all_sources as $source ) : ?>
|
||||
<option value="<?php echo esc_attr( $source['source_key'] ); ?>">
|
||||
<?php echo esc_html( $source['name'] ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-apply-bulk-themes">
|
||||
<?php esc_html_e( '应用', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-toolbar-right">
|
||||
<input type="search" class="wpbridge-search" id="wpbridge-search-themes" placeholder="<?php esc_attr_e( '搜索主题...', 'wpbridge' ); ?>" autocomplete="off">
|
||||
</div>
|
||||
<div class="wpbridge-toolbar-left">
|
||||
<label class="wpbridge-checkbox-all">
|
||||
<input type="checkbox" id="wpbridge-select-all-themes">
|
||||
<span><?php esc_html_e( '全选', 'wpbridge' ); ?></span>
|
||||
</label>
|
||||
<select id="wpbridge-bulk-action-themes" class="wpbridge-select">
|
||||
<option value=""><?php esc_html_e( '批量操作', 'wpbridge' ); ?></option>
|
||||
<option value="set_source"><?php esc_html_e( '设置更新源', 'wpbridge' ); ?></option>
|
||||
<option value="reset_default"><?php esc_html_e( '重置为默认', 'wpbridge' ); ?></option>
|
||||
<option value="disable"><?php esc_html_e( '禁用更新', 'wpbridge' ); ?></option>
|
||||
</select>
|
||||
<select id="wpbridge-bulk-source-themes" class="wpbridge-select wpbridge-bulk-source-select" style="display: none;">
|
||||
<option value=""><?php esc_html_e( '-- 选择更新源 --', 'wpbridge' ); ?></option>
|
||||
<?php foreach ( $all_sources as $source ) : ?>
|
||||
<option value="<?php echo esc_attr( $source['source_key'] ); ?>">
|
||||
<?php echo esc_html( $source['name'] ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm" id="wpbridge-apply-bulk-themes">
|
||||
<?php esc_html_e( '应用', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-toolbar-right">
|
||||
<input type="search" class="wpbridge-search" id="wpbridge-search-themes" placeholder="<?php esc_attr_e( '搜索主题...', 'wpbridge' ); ?>" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主题列表 -->
|
||||
<div class="wpbridge-project-list" id="wpbridge-themes-list">
|
||||
<?php if ( empty( $installed_themes ) ) : ?>
|
||||
<div class="wpbridge-empty">
|
||||
<span class="dashicons dashicons-admin-appearance"></span>
|
||||
<h3><?php esc_html_e( '暂无已安装主题', 'wpbridge' ); ?></h3>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<?php
|
||||
foreach ( $installed_themes as $theme_slug => $theme ) :
|
||||
$item_key = 'theme:' . $theme_slug;
|
||||
$config = $item_manager->get( $item_key );
|
||||
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;
|
||||
<?php if ( empty( $installed_themes ) ) : ?>
|
||||
<div class="wpbridge-empty">
|
||||
<span class="dashicons dashicons-admin-appearance"></span>
|
||||
<h3><?php esc_html_e( '暂无已安装主题', 'wpbridge' ); ?></h3>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<?php foreach ( $installed_themes as $theme_slug => $theme ) :
|
||||
$item_key = 'theme:' . $theme_slug;
|
||||
$config = $item_manager->get( $item_key );
|
||||
$mode = $config['mode'] ?? ItemSourceManager::MODE_DEFAULT;
|
||||
|
||||
// 判断是否当前主题
|
||||
$is_active = $theme_slug === $current_theme_slug;
|
||||
?>
|
||||
<div class="wpbridge-project-item" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-item-type="theme">
|
||||
<div class="wpbridge-project-checkbox">
|
||||
<input type="checkbox" class="wpbridge-project-select" value="<?php echo esc_attr( $item_key ); ?>">
|
||||
</div>
|
||||
// 判断是否当前主题
|
||||
$is_active = $theme_slug === $current_theme_slug;
|
||||
?>
|
||||
<div class="wpbridge-project-item" data-item-key="<?php echo esc_attr( $item_key ); ?>" data-item-type="theme">
|
||||
<div class="wpbridge-project-checkbox">
|
||||
<input type="checkbox" class="wpbridge-project-select" value="<?php echo esc_attr( $item_key ); ?>">
|
||||
</div>
|
||||
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-icon wpbridge-project-expand"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
title="<?php esc_attr_e( '展开配置', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-down-alt2"></span>
|
||||
</button>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-icon wpbridge-project-expand"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
title="<?php esc_attr_e( '展开配置', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-down-alt2"></span>
|
||||
</button>
|
||||
|
||||
<div class="wpbridge-project-thumbnail">
|
||||
<?php
|
||||
$screenshot = $theme->get_screenshot();
|
||||
if ( $screenshot ) :
|
||||
// get_screenshot() 返回相对路径,需要构建完整 URL
|
||||
$screenshot_url = $theme->get_stylesheet_directory_uri() . '/' . basename( $screenshot );
|
||||
?>
|
||||
<img loading="lazy" src="<?php echo esc_url( $screenshot_url ); ?>" alt="<?php echo esc_attr( $theme->get( 'Name' ) ); ?>">
|
||||
<?php else : ?>
|
||||
<span class="dashicons dashicons-admin-appearance"></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wpbridge-project-thumbnail">
|
||||
<?php
|
||||
$screenshot = $theme->get_screenshot();
|
||||
if ( $screenshot ) :
|
||||
// get_screenshot() 返回相对路径,需要构建完整 URL
|
||||
$screenshot_url = $theme->get_stylesheet_directory_uri() . '/' . basename( $screenshot );
|
||||
?>
|
||||
<img loading="lazy" src="<?php echo esc_url( $screenshot_url ); ?>" alt="<?php echo esc_attr( $theme->get( 'Name' ) ); ?>">
|
||||
<?php else : ?>
|
||||
<span class="dashicons dashicons-admin-appearance"></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-project-info">
|
||||
<div class="wpbridge-project-name">
|
||||
<?php echo esc_html( $theme->get( 'Name' ) ); ?>
|
||||
<?php if ( $is_active ) : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-success"><?php esc_html_e( '当前主题', 'wpbridge' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wpbridge-project-meta">
|
||||
<span class="wpbridge-project-version">v<?php echo esc_html( $theme->get( 'Version' ) ); ?></span>
|
||||
<span class="wpbridge-project-slug"><?php echo esc_html( $theme_slug ); ?></span>
|
||||
<a href="#" class="wpbridge-view-changelog"
|
||||
data-slug="<?php echo esc_attr( $theme_slug ); ?>"
|
||||
data-type="theme"
|
||||
data-source-type="wporg"
|
||||
title="<?php esc_attr_e( '查看更新日志', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-list-view"></span>
|
||||
</a>
|
||||
<?php if ( $theme->get( 'Author' ) ) : ?>
|
||||
<span class="wpbridge-project-author"><?php echo esc_html( $theme->get( 'Author' ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-project-info">
|
||||
<div class="wpbridge-project-name">
|
||||
<?php echo esc_html( $theme->get( 'Name' ) ); ?>
|
||||
<?php if ( $is_active ) : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-success"><?php esc_html_e( '当前主题', 'wpbridge' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wpbridge-project-meta">
|
||||
<span class="wpbridge-project-version">v<?php echo esc_html( $theme->get( 'Version' ) ); ?></span>
|
||||
<span class="wpbridge-project-slug"><?php echo esc_html( $theme_slug ); ?></span>
|
||||
<a href="#" class="wpbridge-view-changelog"
|
||||
data-slug="<?php echo esc_attr( $theme_slug ); ?>"
|
||||
data-type="theme"
|
||||
data-source-type="wporg"
|
||||
title="<?php esc_attr_e( '查看更新日志', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-list-view"></span>
|
||||
</a>
|
||||
<?php if ( $theme->get( 'Author' ) ) : ?>
|
||||
<span class="wpbridge-project-author"><?php echo esc_html( $theme->get( 'Author' ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-project-status">
|
||||
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-disabled">
|
||||
<span class="dashicons dashicons-dismiss"></span>
|
||||
<?php esc_html_e( '已禁用', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php elseif ( $mode === ItemSourceManager::MODE_CUSTOM ) : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-custom">
|
||||
<span class="dashicons dashicons-admin-links"></span>
|
||||
<?php esc_html_e( '自定义', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-default">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
<?php esc_html_e( '默认', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wpbridge-project-status">
|
||||
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-disabled">
|
||||
<span class="dashicons dashicons-dismiss"></span>
|
||||
<?php esc_html_e( '已禁用', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php elseif ( $mode === ItemSourceManager::MODE_CUSTOM ) : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-custom">
|
||||
<span class="dashicons dashicons-admin-links"></span>
|
||||
<?php esc_html_e( '自定义', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<span class="wpbridge-status-badge wpbridge-status-default">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
<?php esc_html_e( '默认', 'wpbridge' ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 内联配置面板(默认折叠) -->
|
||||
<div class="wpbridge-project-config-panel" data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<input type="url" class="wpbridge-form-input wpbridge-inline-url"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
placeholder="https://github.com/user/repo 或 https://example.com/update.json"
|
||||
autocomplete="off">
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '粘贴更新源地址,系统会自动识别类型', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<input type="password" class="wpbridge-form-input wpbridge-inline-token"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
placeholder="<?php esc_attr_e( '可选,用于私有仓库', 'wpbridge' ); ?>"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-config-actions">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-btn-sm wpbridge-save-inline"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-saved"></span>
|
||||
<?php esc_html_e( '保存', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php if ( $mode !== ItemSourceManager::MODE_DEFAULT ) : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-reset-default"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<?php esc_html_e( '重置为默认', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-enable-update"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '启用更新', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-disable-update"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-dismiss"></span>
|
||||
<?php esc_html_e( '禁用更新', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<!-- 内联配置面板(默认折叠) -->
|
||||
<div class="wpbridge-project-config-panel" data-item-key="<?php echo esc_attr( $item_key ); ?>" style="display: none;">
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<input type="url" class="wpbridge-form-input wpbridge-inline-url"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
placeholder="https://github.com/user/repo 或 https://example.com/update.json"
|
||||
autocomplete="off">
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '粘贴更新源地址,系统会自动识别类型', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-config-row">
|
||||
<label class="wpbridge-config-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
|
||||
<div class="wpbridge-config-field">
|
||||
<input type="password" class="wpbridge-form-input wpbridge-inline-token"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>"
|
||||
placeholder="<?php esc_attr_e( '可选,用于私有仓库', 'wpbridge' ); ?>"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-config-actions">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-btn-sm wpbridge-save-inline"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-saved"></span>
|
||||
<?php esc_html_e( '保存', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php if ( $mode !== ItemSourceManager::MODE_DEFAULT ) : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-reset-default"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<?php esc_html_e( '重置为默认', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ( $mode === ItemSourceManager::MODE_DISABLED ) : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-enable-update"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '启用更新', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php else : ?>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-disable-update"
|
||||
data-item-key="<?php echo esc_attr( $item_key ); ?>">
|
||||
<span class="dashicons dashicons-dismiss"></span>
|
||||
<?php esc_html_e( '禁用更新', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,137 +9,137 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap wpbridge-wrap">
|
||||
<h1><?php esc_html_e( 'WPBridge 设置', 'wpbridge' ); ?></h1>
|
||||
<h1><?php esc_html_e( 'WPBridge 设置', 'wpbridge' ); ?></h1>
|
||||
|
||||
<?php settings_errors( 'wpbridge' ); ?>
|
||||
<?php settings_errors( 'wpbridge' ); ?>
|
||||
|
||||
<form method="post">
|
||||
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||
<input type="hidden" name="wpbridge_action" value="save_settings">
|
||||
<form method="post">
|
||||
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||
<input type="hidden" name="wpbridge_action" value="save_settings">
|
||||
|
||||
<h2><?php esc_html_e( '常规设置', 'wpbridge' ); ?></h2>
|
||||
<h2><?php esc_html_e( '常规设置', 'wpbridge' ); ?></h2>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( '调试模式', 'wpbridge' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="debug_mode"
|
||||
value="1"
|
||||
<?php checked( $settings['debug_mode'] ?? false ); ?>>
|
||||
<?php esc_html_e( '启用调试日志', 'wpbridge' ); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php esc_html_e( '启用后会记录详细的调试信息,仅在排查问题时启用。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( '调试模式', 'wpbridge' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="debug_mode"
|
||||
value="1"
|
||||
<?php checked( $settings['debug_mode'] ?? false ); ?>>
|
||||
<?php esc_html_e( '启用调试日志', 'wpbridge' ); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php esc_html_e( '启用后会记录详细的调试信息,仅在排查问题时启用。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wpbridge-cache-ttl"><?php esc_html_e( '缓存时间', 'wpbridge' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="wpbridge-cache-ttl" name="cache_ttl">
|
||||
<option value="3600" <?php selected( $settings['cache_ttl'] ?? 43200, 3600 ); ?>>
|
||||
<?php esc_html_e( '1 小时', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="21600" <?php selected( $settings['cache_ttl'] ?? 43200, 21600 ); ?>>
|
||||
<?php esc_html_e( '6 小时', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="43200" <?php selected( $settings['cache_ttl'] ?? 43200, 43200 ); ?>>
|
||||
<?php esc_html_e( '12 小时', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="86400" <?php selected( $settings['cache_ttl'] ?? 43200, 86400 ); ?>>
|
||||
<?php esc_html_e( '24 小时', 'wpbridge' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php esc_html_e( '更新检查结果的缓存时间', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wpbridge-cache-ttl"><?php esc_html_e( '缓存时间', 'wpbridge' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="wpbridge-cache-ttl" name="cache_ttl">
|
||||
<option value="3600" <?php selected( $settings['cache_ttl'] ?? 43200, 3600 ); ?>>
|
||||
<?php esc_html_e( '1 小时', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="21600" <?php selected( $settings['cache_ttl'] ?? 43200, 21600 ); ?>>
|
||||
<?php esc_html_e( '6 小时', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="43200" <?php selected( $settings['cache_ttl'] ?? 43200, 43200 ); ?>>
|
||||
<?php esc_html_e( '12 小时', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="86400" <?php selected( $settings['cache_ttl'] ?? 43200, 86400 ); ?>>
|
||||
<?php esc_html_e( '24 小时', 'wpbridge' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php esc_html_e( '更新检查结果的缓存时间', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wpbridge-timeout"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number"
|
||||
id="wpbridge-timeout"
|
||||
name="request_timeout"
|
||||
value="<?php echo esc_attr( $settings['request_timeout'] ?? 10 ); ?>"
|
||||
min="5"
|
||||
max="60"
|
||||
class="small-text">
|
||||
<?php esc_html_e( '秒', 'wpbridge' ); ?>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'HTTP 请求的超时时间(5-60 秒)', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wpbridge-timeout"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number"
|
||||
id="wpbridge-timeout"
|
||||
name="request_timeout"
|
||||
value="<?php echo esc_attr( $settings['request_timeout'] ?? 10 ); ?>"
|
||||
min="5"
|
||||
max="60"
|
||||
class="small-text">
|
||||
<?php esc_html_e( '秒', 'wpbridge' ); ?>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'HTTP 请求的超时时间(5-60 秒)', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( '降级策略', 'wpbridge' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="fallback_enabled"
|
||||
value="1"
|
||||
<?php checked( $settings['fallback_enabled'] ?? true ); ?>>
|
||||
<?php esc_html_e( '启用过期缓存兜底', 'wpbridge' ); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php esc_html_e( '当更新源不可用时,使用过期的缓存数据。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( '降级策略', 'wpbridge' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="fallback_enabled"
|
||||
value="1"
|
||||
<?php checked( $settings['fallback_enabled'] ?? true ); ?>>
|
||||
<?php esc_html_e( '启用过期缓存兜底', 'wpbridge' ); ?>
|
||||
</label>
|
||||
<p class="description">
|
||||
<?php esc_html_e( '当更新源不可用时,使用过期的缓存数据。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<input type="submit" class="button button-primary" value="<?php esc_attr_e( '保存设置', 'wpbridge' ); ?>">
|
||||
</p>
|
||||
</form>
|
||||
<p class="submit">
|
||||
<input type="submit" class="button button-primary" value="<?php esc_attr_e( '保存设置', 'wpbridge' ); ?>">
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
<hr>
|
||||
|
||||
<h2><?php esc_html_e( '调试日志', 'wpbridge' ); ?></h2>
|
||||
<h2><?php esc_html_e( '调试日志', 'wpbridge' ); ?></h2>
|
||||
|
||||
<?php if ( empty( $logs ) ) : ?>
|
||||
<p><?php esc_html_e( '暂无日志', 'wpbridge' ); ?></p>
|
||||
<?php else : ?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 150px;"><?php esc_html_e( '时间', 'wpbridge' ); ?></th>
|
||||
<th style="width: 80px;"><?php esc_html_e( '级别', 'wpbridge' ); ?></th>
|
||||
<th><?php esc_html_e( '消息', 'wpbridge' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( $log['time'] ); ?></td>
|
||||
<td>
|
||||
<span class="wpbridge-log-level wpbridge-log-<?php echo esc_attr( $log['level'] ); ?>">
|
||||
<?php echo esc_html( strtoupper( $log['level'] ) ); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo esc_html( $log['message'] ); ?>
|
||||
<?php if ( ! empty( $log['context'] ) ) : ?>
|
||||
<code><?php echo esc_html( wp_json_encode( $log['context'] ) ); ?></code>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
<?php if ( empty( $logs ) ) : ?>
|
||||
<p><?php esc_html_e( '暂无日志', 'wpbridge' ); ?></p>
|
||||
<?php else : ?>
|
||||
<table class="wp-list-table widefat fixed striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 150px;"><?php esc_html_e( '时间', 'wpbridge' ); ?></th>
|
||||
<th style="width: 80px;"><?php esc_html_e( '级别', 'wpbridge' ); ?></th>
|
||||
<th><?php esc_html_e( '消息', 'wpbridge' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( $log['time'] ); ?></td>
|
||||
<td>
|
||||
<span class="wpbridge-log-level wpbridge-log-<?php echo esc_attr( $log['level'] ); ?>">
|
||||
<?php echo esc_html( strtoupper( $log['level'] ) ); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo esc_html( $log['message'] ); ?>
|
||||
<?php if ( ! empty( $log['context'] ) ) : ?>
|
||||
<code><?php echo esc_html( wp_json_encode( $log['context'] ) ); ?></code>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
$is_edit = null !== $source;
|
||||
|
|
@ -19,195 +19,195 @@ $title = $is_edit ? __( '编辑更新源', 'wpbridge' ) : __( '添加更新源
|
|||
|
||||
<!-- 标题栏 -->
|
||||
<header class="wpbridge-header">
|
||||
<div class="wpbridge-header-left">
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge' ) ); ?>" class="wpbridge-back-link">
|
||||
<span class="dashicons dashicons-arrow-left-alt2"></span>
|
||||
</a>
|
||||
<h1 class="wpbridge-title"><?php echo esc_html( $title ); ?></h1>
|
||||
</div>
|
||||
<div class="wpbridge-header-left">
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge' ) ); ?>" class="wpbridge-back-link">
|
||||
<span class="dashicons dashicons-arrow-left-alt2"></span>
|
||||
</a>
|
||||
<h1 class="wpbridge-title"><?php echo esc_html( $title ); ?></h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="wrap wpbridge-wrap">
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="wpbridge-content">
|
||||
<div class="wpbridge-tabs-card">
|
||||
<div class="wpbridge-tab-pane wpbridge-tab-pane-active" style="padding: 24px;">
|
||||
<?php settings_errors( 'wpbridge' ); ?>
|
||||
<!-- 主内容区 -->
|
||||
<div class="wpbridge-content">
|
||||
<div class="wpbridge-tabs-card">
|
||||
<div class="wpbridge-tab-pane wpbridge-tab-pane-active" style="padding: 24px;">
|
||||
<?php settings_errors( 'wpbridge' ); ?>
|
||||
|
||||
<form method="post" class="wpbridge-editor-form">
|
||||
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||
<input type="hidden" name="wpbridge_action" value="save_source">
|
||||
<input type="hidden" name="source_id" value="<?php echo esc_attr( $source->id ?? '' ); ?>">
|
||||
<form method="post" class="wpbridge-editor-form">
|
||||
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||
<input type="hidden" name="wpbridge_action" value="save_source">
|
||||
<input type="hidden" name="source_id" value="<?php echo esc_attr( $source->id ?? '' ); ?>">
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="wpbridge-form-section">
|
||||
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '基本信息', 'wpbridge' ); ?></h2>
|
||||
<!-- 基本信息 -->
|
||||
<div class="wpbridge-form-section">
|
||||
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '基本信息', 'wpbridge' ); ?></h2>
|
||||
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label">
|
||||
<?php esc_html_e( '名称', 'wpbridge' ); ?>
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<div>
|
||||
<input type="text"
|
||||
name="name"
|
||||
value="<?php echo esc_attr( $source->name ?? '' ); ?>"
|
||||
class="wpbridge-form-input"
|
||||
placeholder="<?php esc_attr_e( '例如:我的私有仓库', 'wpbridge' ); ?>"
|
||||
required>
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '更新源的显示名称', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label">
|
||||
<?php esc_html_e( '名称', 'wpbridge' ); ?>
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<div>
|
||||
<input type="text"
|
||||
name="name"
|
||||
value="<?php echo esc_attr( $source->name ?? '' ); ?>"
|
||||
class="wpbridge-form-input"
|
||||
placeholder="<?php esc_attr_e( '例如:我的私有仓库', 'wpbridge' ); ?>"
|
||||
required>
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '更新源的显示名称', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label">
|
||||
<?php esc_html_e( '类型', 'wpbridge' ); ?>
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<div>
|
||||
<select name="type" class="wpbridge-form-select">
|
||||
<?php foreach ( $types as $type_value => $type_label ) : ?>
|
||||
<option value="<?php echo esc_attr( $type_value ); ?>"
|
||||
<?php selected( $source->type ?? 'json', $type_value ); ?>>
|
||||
<?php echo esc_html( $type_label ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '选择更新源的类型', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label">
|
||||
<?php esc_html_e( '类型', 'wpbridge' ); ?>
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<div>
|
||||
<select name="type" class="wpbridge-form-select">
|
||||
<?php foreach ( $types as $type_value => $type_label ) : ?>
|
||||
<option value="<?php echo esc_attr( $type_value ); ?>"
|
||||
<?php selected( $source->type ?? 'json', $type_value ); ?>>
|
||||
<?php echo esc_html( $type_label ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '选择更新源的类型', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label">
|
||||
<?php esc_html_e( '更新地址', 'wpbridge' ); ?>
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<div>
|
||||
<input type="url"
|
||||
name="api_url"
|
||||
value="<?php echo esc_url( $source->api_url ?? '' ); ?>"
|
||||
class="wpbridge-form-input"
|
||||
style="max-width: 100%;"
|
||||
placeholder="https://example.com/api/v1"
|
||||
required>
|
||||
<p class="wpbridge-form-help">
|
||||
<?php esc_html_e( '更新源的地址。对于 JSON 类型,可以使用 {slug} 占位符。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label">
|
||||
<?php esc_html_e( '更新地址', 'wpbridge' ); ?>
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<div>
|
||||
<input type="url"
|
||||
name="api_url"
|
||||
value="<?php echo esc_url( $source->api_url ?? '' ); ?>"
|
||||
class="wpbridge-form-input"
|
||||
style="max-width: 100%;"
|
||||
placeholder="https://example.com/api/v1"
|
||||
required>
|
||||
<p class="wpbridge-form-help">
|
||||
<?php esc_html_e( '更新源的地址。对于 JSON 类型,可以使用 {slug} 占位符。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 匹配规则 -->
|
||||
<div class="wpbridge-form-section">
|
||||
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '匹配规则', 'wpbridge' ); ?></h2>
|
||||
<!-- 匹配规则 -->
|
||||
<div class="wpbridge-form-section">
|
||||
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '匹配规则', 'wpbridge' ); ?></h2>
|
||||
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label"><?php esc_html_e( '项目类型', 'wpbridge' ); ?></label>
|
||||
<div>
|
||||
<select name="item_type" class="wpbridge-form-select">
|
||||
<option value="plugin" <?php selected( $source->item_type ?? 'plugin', 'plugin' ); ?>>
|
||||
<?php esc_html_e( '插件', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="theme" <?php selected( $source->item_type ?? 'plugin', 'theme' ); ?>>
|
||||
<?php esc_html_e( '主题', 'wpbridge' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '此更新源用于插件还是主题', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label"><?php esc_html_e( '项目类型', 'wpbridge' ); ?></label>
|
||||
<div>
|
||||
<select name="item_type" class="wpbridge-form-select">
|
||||
<option value="plugin" <?php selected( $source->item_type ?? 'plugin', 'plugin' ); ?>>
|
||||
<?php esc_html_e( '插件', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="theme" <?php selected( $source->item_type ?? 'plugin', 'theme' ); ?>>
|
||||
<?php esc_html_e( '主题', 'wpbridge' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="wpbridge-form-help"><?php esc_html_e( '此更新源用于插件还是主题', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label"><?php esc_html_e( '插件/主题标识', 'wpbridge' ); ?></label>
|
||||
<div>
|
||||
<input type="text"
|
||||
name="slug"
|
||||
value="<?php echo esc_attr( $source->slug ?? '' ); ?>"
|
||||
class="wpbridge-form-input"
|
||||
placeholder="<?php esc_attr_e( '留空匹配所有', 'wpbridge' ); ?>">
|
||||
<p class="wpbridge-form-help">
|
||||
<?php esc_html_e( '指定插件/主题的标识名称(通常是文件夹名)。留空表示匹配所有。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label"><?php esc_html_e( '插件/主题标识', 'wpbridge' ); ?></label>
|
||||
<div>
|
||||
<input type="text"
|
||||
name="slug"
|
||||
value="<?php echo esc_attr( $source->slug ?? '' ); ?>"
|
||||
class="wpbridge-form-input"
|
||||
placeholder="<?php esc_attr_e( '留空匹配所有', 'wpbridge' ); ?>">
|
||||
<p class="wpbridge-form-help">
|
||||
<?php esc_html_e( '指定插件/主题的标识名称(通常是文件夹名)。留空表示匹配所有。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label"><?php esc_html_e( '首选程度', 'wpbridge' ); ?></label>
|
||||
<div>
|
||||
<?php
|
||||
$current_priority = $source->priority ?? 50;
|
||||
// 将数字优先级映射到语义化选项
|
||||
if ( $current_priority <= 20 ) {
|
||||
$priority_level = 'primary';
|
||||
} elseif ( $current_priority <= 60 ) {
|
||||
$priority_level = 'secondary';
|
||||
} else {
|
||||
$priority_level = 'fallback';
|
||||
}
|
||||
?>
|
||||
<select name="priority_level" class="wpbridge-form-select">
|
||||
<option value="primary" <?php selected( $priority_level, 'primary' ); ?>>
|
||||
<?php esc_html_e( '首选源 - 优先使用此源', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="secondary" <?php selected( $priority_level, 'secondary' ); ?>>
|
||||
<?php esc_html_e( '备选源 - 首选不可用时使用', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="fallback" <?php selected( $priority_level, 'fallback' ); ?>>
|
||||
<?php esc_html_e( '最后选择 - 其他源都不可用时使用', 'wpbridge' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="wpbridge-form-help">
|
||||
<?php esc_html_e( '当多个源可用时,按此顺序尝试获取更新', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label"><?php esc_html_e( '首选程度', 'wpbridge' ); ?></label>
|
||||
<div>
|
||||
<?php
|
||||
$current_priority = $source->priority ?? 50;
|
||||
// 将数字优先级映射到语义化选项
|
||||
if ( $current_priority <= 20 ) {
|
||||
$priority_level = 'primary';
|
||||
} elseif ( $current_priority <= 60 ) {
|
||||
$priority_level = 'secondary';
|
||||
} else {
|
||||
$priority_level = 'fallback';
|
||||
}
|
||||
?>
|
||||
<select name="priority_level" class="wpbridge-form-select">
|
||||
<option value="primary" <?php selected( $priority_level, 'primary' ); ?>>
|
||||
<?php esc_html_e( '首选源 - 优先使用此源', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="secondary" <?php selected( $priority_level, 'secondary' ); ?>>
|
||||
<?php esc_html_e( '备选源 - 首选不可用时使用', 'wpbridge' ); ?>
|
||||
</option>
|
||||
<option value="fallback" <?php selected( $priority_level, 'fallback' ); ?>>
|
||||
<?php esc_html_e( '最后选择 - 其他源都不可用时使用', 'wpbridge' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="wpbridge-form-help">
|
||||
<?php esc_html_e( '当多个源可用时,按此顺序尝试获取更新', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 认证设置 -->
|
||||
<div class="wpbridge-form-section">
|
||||
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '认证设置', 'wpbridge' ); ?></h2>
|
||||
<!-- 认证设置 -->
|
||||
<div class="wpbridge-form-section">
|
||||
<h2 class="wpbridge-form-section-title"><?php esc_html_e( '认证设置', 'wpbridge' ); ?></h2>
|
||||
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
|
||||
<div>
|
||||
<input type="password"
|
||||
name="auth_token"
|
||||
value="<?php echo esc_attr( ! empty( $source->auth_token ) ? '********' : '' ); ?>"
|
||||
class="wpbridge-form-input"
|
||||
autocomplete="new-password"
|
||||
placeholder="<?php echo esc_attr( ! empty( $source->auth_token ) ? __( '已设置(留空保持不变)', 'wpbridge' ) : __( '可选', 'wpbridge' ) ); ?>">
|
||||
<p class="wpbridge-form-help">
|
||||
<?php esc_html_e( '用于私有仓库或需要认证的更新源。留空表示无需认证。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label"><?php esc_html_e( '访问密码', 'wpbridge' ); ?></label>
|
||||
<div>
|
||||
<input type="password"
|
||||
name="auth_token"
|
||||
value="<?php echo esc_attr( ! empty( $source->auth_token ) ? '********' : '' ); ?>"
|
||||
class="wpbridge-form-input"
|
||||
autocomplete="new-password"
|
||||
placeholder="<?php echo esc_attr( ! empty( $source->auth_token ) ? __( '已设置(留空保持不变)', 'wpbridge' ) : __( '可选', 'wpbridge' ) ); ?>">
|
||||
<p class="wpbridge-form-help">
|
||||
<?php esc_html_e( '用于私有仓库或需要认证的更新源。留空表示无需认证。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label"><?php esc_html_e( '启用状态', 'wpbridge' ); ?></label>
|
||||
<div>
|
||||
<label class="wpbridge-toggle">
|
||||
<input type="checkbox" name="enabled" value="1" <?php checked( $source->enabled ?? true ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
<p class="wpbridge-form-help" style="margin-top: 8px;">
|
||||
<?php esc_html_e( '启用此更新源', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-form-row">
|
||||
<label class="wpbridge-form-label"><?php esc_html_e( '启用状态', 'wpbridge' ); ?></label>
|
||||
<div>
|
||||
<label class="wpbridge-toggle">
|
||||
<input type="checkbox" name="enabled" value="1" <?php checked( $source->enabled ?? true ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
<p class="wpbridge-form-help" style="margin-top: 8px;">
|
||||
<?php esc_html_e( '启用此更新源', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="wpbridge-form-actions">
|
||||
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
|
||||
<span class="dashicons dashicons-saved"></span>
|
||||
<?php echo esc_html( $is_edit ? __( '保存更改', 'wpbridge' ) : __( '添加更新源', 'wpbridge' ) ); ?>
|
||||
</button>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge' ) ); ?>" class="wpbridge-btn wpbridge-btn-secondary">
|
||||
<?php esc_html_e( '取消', 'wpbridge' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 操作按钮 -->
|
||||
<div class="wpbridge-form-actions">
|
||||
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
|
||||
<span class="dashicons dashicons-saved"></span>
|
||||
<?php echo esc_html( $is_edit ? __( '保存更改', 'wpbridge' ) : __( '添加更新源', 'wpbridge' ) ); ?>
|
||||
</button>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge' ) ); ?>" class="wpbridge-btn wpbridge-btn-secondary">
|
||||
<?php esc_html_e( '取消', 'wpbridge' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,121 +9,121 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
use WPBridge\UpdateSource\SourceType;
|
||||
?>
|
||||
|
||||
<div class="wrap wpbridge-wrap">
|
||||
<h1 class="wp-heading-inline"><?php esc_html_e( 'WPBridge 更新源', 'wpbridge' ); ?></h1>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="page-title-action">
|
||||
<?php esc_html_e( '添加更新源', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<hr class="wp-header-end">
|
||||
<h1 class="wp-heading-inline"><?php esc_html_e( 'WPBridge 更新源', 'wpbridge' ); ?></h1>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="page-title-action">
|
||||
<?php esc_html_e( '添加更新源', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<hr class="wp-header-end">
|
||||
|
||||
<?php settings_errors( 'wpbridge' ); ?>
|
||||
<?php settings_errors( 'wpbridge' ); ?>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="wpbridge-stats">
|
||||
<div class="wpbridge-stat-item">
|
||||
<span class="wpbridge-stat-number"><?php echo esc_html( $stats['total'] ); ?></span>
|
||||
<span class="wpbridge-stat-label"><?php esc_html_e( '总数', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-stat-item">
|
||||
<span class="wpbridge-stat-number"><?php echo esc_html( $stats['enabled'] ); ?></span>
|
||||
<span class="wpbridge-stat-label"><?php esc_html_e( '已启用', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-stat-item">
|
||||
<button type="button" class="button wpbridge-clear-cache">
|
||||
<?php esc_html_e( '清除缓存', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 统计信息 -->
|
||||
<div class="wpbridge-stats">
|
||||
<div class="wpbridge-stat-item">
|
||||
<span class="wpbridge-stat-number"><?php echo esc_html( $stats['total'] ); ?></span>
|
||||
<span class="wpbridge-stat-label"><?php esc_html_e( '总数', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-stat-item">
|
||||
<span class="wpbridge-stat-number"><?php echo esc_html( $stats['enabled'] ); ?></span>
|
||||
<span class="wpbridge-stat-label"><?php esc_html_e( '已启用', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-stat-item">
|
||||
<button type="button" class="button wpbridge-clear-cache">
|
||||
<?php esc_html_e( '清除缓存', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 源列表 -->
|
||||
<table class="wp-list-table widefat fixed striped wpbridge-sources-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="column-status"><?php esc_html_e( '状态', 'wpbridge' ); ?></th>
|
||||
<th class="column-name"><?php esc_html_e( '名称', 'wpbridge' ); ?></th>
|
||||
<th class="column-type"><?php esc_html_e( '类型', 'wpbridge' ); ?></th>
|
||||
<th class="column-slug"><?php esc_html_e( 'Slug', 'wpbridge' ); ?></th>
|
||||
<th class="column-priority"><?php esc_html_e( '优先级', 'wpbridge' ); ?></th>
|
||||
<th class="column-actions"><?php esc_html_e( '操作', 'wpbridge' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ( empty( $sources ) ) : ?>
|
||||
<tr>
|
||||
<td colspan="6" class="wpbridge-no-items">
|
||||
<?php esc_html_e( '暂无更新源', 'wpbridge' ); ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ( $sources as $source ) : ?>
|
||||
<tr data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||
<td class="column-status">
|
||||
<label class="wpbridge-toggle">
|
||||
<input type="checkbox"
|
||||
class="wpbridge-toggle-source"
|
||||
<?php checked( $source->enabled ); ?>
|
||||
data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||
<span class="wpbridge-toggle-slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="column-name">
|
||||
<strong>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=edit&source=' . $source->id ) ); ?>">
|
||||
<?php echo esc_html( $source->name ?: $source->id ); ?>
|
||||
</a>
|
||||
</strong>
|
||||
<?php if ( $source->is_preset ) : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-preset"><?php esc_html_e( '预置', 'wpbridge' ); ?></span>
|
||||
<?php endif; ?>
|
||||
<div class="wpbridge-source-url">
|
||||
<?php echo esc_html( $source->api_url ); ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="column-type">
|
||||
<span class="wpbridge-type-badge wpbridge-type-<?php echo esc_attr( $source->type ); ?>">
|
||||
<?php echo esc_html( SourceType::get_label( $source->type ) ); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="column-slug">
|
||||
<?php echo esc_html( $source->slug ?: __( '全部', 'wpbridge' ) ); ?>
|
||||
</td>
|
||||
<td class="column-priority">
|
||||
<?php echo esc_html( $source->priority ); ?>
|
||||
</td>
|
||||
<td class="column-actions">
|
||||
<button type="button"
|
||||
class="button button-small wpbridge-test-source"
|
||||
data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||
<?php esc_html_e( '测试', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=edit&source=' . $source->id ) ); ?>"
|
||||
class="button button-small">
|
||||
<?php esc_html_e( '编辑', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<?php if ( ! $source->is_preset ) : ?>
|
||||
<button type="button"
|
||||
class="button button-small button-link-delete wpbridge-delete-source"
|
||||
data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||
<?php esc_html_e( '删除', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- 源列表 -->
|
||||
<table class="wp-list-table widefat fixed striped wpbridge-sources-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="column-status"><?php esc_html_e( '状态', 'wpbridge' ); ?></th>
|
||||
<th class="column-name"><?php esc_html_e( '名称', 'wpbridge' ); ?></th>
|
||||
<th class="column-type"><?php esc_html_e( '类型', 'wpbridge' ); ?></th>
|
||||
<th class="column-slug"><?php esc_html_e( 'Slug', 'wpbridge' ); ?></th>
|
||||
<th class="column-priority"><?php esc_html_e( '优先级', 'wpbridge' ); ?></th>
|
||||
<th class="column-actions"><?php esc_html_e( '操作', 'wpbridge' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ( empty( $sources ) ) : ?>
|
||||
<tr>
|
||||
<td colspan="6" class="wpbridge-no-items">
|
||||
<?php esc_html_e( '暂无更新源', 'wpbridge' ); ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ( $sources as $source ) : ?>
|
||||
<tr data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||
<td class="column-status">
|
||||
<label class="wpbridge-toggle">
|
||||
<input type="checkbox"
|
||||
class="wpbridge-toggle-source"
|
||||
<?php checked( $source->enabled ); ?>
|
||||
data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||
<span class="wpbridge-toggle-slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="column-name">
|
||||
<strong>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=edit&source=' . $source->id ) ); ?>">
|
||||
<?php echo esc_html( $source->name ?: $source->id ); ?>
|
||||
</a>
|
||||
</strong>
|
||||
<?php if ( $source->is_preset ) : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-preset"><?php esc_html_e( '预置', 'wpbridge' ); ?></span>
|
||||
<?php endif; ?>
|
||||
<div class="wpbridge-source-url">
|
||||
<?php echo esc_html( $source->api_url ); ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="column-type">
|
||||
<span class="wpbridge-type-badge wpbridge-type-<?php echo esc_attr( $source->type ); ?>">
|
||||
<?php echo esc_html( SourceType::get_label( $source->type ) ); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="column-slug">
|
||||
<?php echo esc_html( $source->slug ?: __( '全部', 'wpbridge' ) ); ?>
|
||||
</td>
|
||||
<td class="column-priority">
|
||||
<?php echo esc_html( $source->priority ); ?>
|
||||
</td>
|
||||
<td class="column-actions">
|
||||
<button type="button"
|
||||
class="button button-small wpbridge-test-source"
|
||||
data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||
<?php esc_html_e( '测试', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=edit&source=' . $source->id ) ); ?>"
|
||||
class="button button-small">
|
||||
<?php esc_html_e( '编辑', 'wpbridge' ); ?>
|
||||
</a>
|
||||
<?php if ( ! $source->is_preset ) : ?>
|
||||
<button type="button"
|
||||
class="button button-small button-link-delete wpbridge-delete-source"
|
||||
data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||
<?php esc_html_e( '删除', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 删除确认表单 -->
|
||||
<form id="wpbridge-delete-form" method="post" style="display: none;">
|
||||
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||
<input type="hidden" name="wpbridge_action" value="delete_source">
|
||||
<input type="hidden" name="source_id" id="wpbridge-delete-source-id" value="">
|
||||
</form>
|
||||
<!-- 删除确认表单 -->
|
||||
<form id="wpbridge-delete-form" method="post" style="display: none;">
|
||||
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||
<input type="hidden" name="wpbridge_action" value="delete_source">
|
||||
<input type="hidden" name="source_id" id="wpbridge-delete-source-id" value="">
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,182 +9,182 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
$api_settings = $settings['api'] ?? array();
|
||||
$api_settings = $settings['api'] ?? [];
|
||||
$api_enabled = ! empty( $api_settings['enabled'] );
|
||||
$require_auth = ! empty( $api_settings['require_auth'] );
|
||||
$rate_limit = $api_settings['rate_limit'] ?? 100;
|
||||
$api_keys = $api_settings['keys'] ?? array();
|
||||
$api_keys = $api_settings['keys'] ?? [];
|
||||
?>
|
||||
|
||||
<form method="post" class="wpbridge-api-form">
|
||||
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||
<input type="hidden" name="wpbridge_action" value="save_api_settings">
|
||||
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||
<input type="hidden" name="wpbridge_action" value="save_api_settings">
|
||||
|
||||
<!-- API 状态面板 -->
|
||||
<div class="wpbridge-stats-panel" style="margin-bottom: 24px;">
|
||||
<div class="wpbridge-stat-card">
|
||||
<div class="wpbridge-stat-card-header">
|
||||
<span class="dashicons dashicons-rest-api"></span>
|
||||
<?php esc_html_e( 'API 状态', 'wpbridge' ); ?>
|
||||
</div>
|
||||
<div class="wpbridge-stat-value <?php echo $api_enabled ? 'success' : ''; ?>">
|
||||
<?php echo $api_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-stat-card">
|
||||
<div class="wpbridge-stat-card-header">
|
||||
<span class="dashicons dashicons-admin-network"></span>
|
||||
<?php esc_html_e( 'API 端点', 'wpbridge' ); ?>
|
||||
</div>
|
||||
<div style="font-size: 12px; font-family: var(--wpbridge-font-mono); color: var(--wpbridge-gray-600); word-break: break-all;">
|
||||
<?php echo esc_html( rest_url( 'bridge/v1/' ) ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-stat-card">
|
||||
<div class="wpbridge-stat-card-header">
|
||||
<span class="dashicons dashicons-admin-users"></span>
|
||||
<?php esc_html_e( 'API Keys', 'wpbridge' ); ?>
|
||||
</div>
|
||||
<div class="wpbridge-stat-value"><?php echo count( $api_keys ); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- API 状态面板 -->
|
||||
<div class="wpbridge-stats-panel" style="margin-bottom: 24px;">
|
||||
<div class="wpbridge-stat-card">
|
||||
<div class="wpbridge-stat-card-header">
|
||||
<span class="dashicons dashicons-rest-api"></span>
|
||||
<?php esc_html_e( 'API 状态', 'wpbridge' ); ?>
|
||||
</div>
|
||||
<div class="wpbridge-stat-value <?php echo $api_enabled ? 'success' : ''; ?>">
|
||||
<?php echo $api_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-stat-card">
|
||||
<div class="wpbridge-stat-card-header">
|
||||
<span class="dashicons dashicons-admin-network"></span>
|
||||
<?php esc_html_e( 'API 端点', 'wpbridge' ); ?>
|
||||
</div>
|
||||
<div style="font-size: 12px; font-family: var(--wpbridge-font-mono); color: var(--wpbridge-gray-600); word-break: break-all;">
|
||||
<?php echo esc_html( rest_url( 'bridge/v1/' ) ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-stat-card">
|
||||
<div class="wpbridge-stat-card-header">
|
||||
<span class="dashicons dashicons-admin-users"></span>
|
||||
<?php esc_html_e( 'API Keys', 'wpbridge' ); ?>
|
||||
</div>
|
||||
<div class="wpbridge-stat-value"><?php echo count( $api_keys ); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-settings-panel">
|
||||
<!-- 启用 API -->
|
||||
<div class="wpbridge-settings-row">
|
||||
<div class="wpbridge-settings-info">
|
||||
<h3 class="wpbridge-settings-title"><?php esc_html_e( '启用 Bridge API', 'wpbridge' ); ?></h3>
|
||||
<p class="wpbridge-settings-desc"><?php esc_html_e( '允许通过 REST API 远程访问 WPBridge 功能。', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<label class="wpbridge-toggle">
|
||||
<input type="checkbox" name="api_enabled" value="1" <?php checked( $api_enabled ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="wpbridge-settings-panel">
|
||||
<!-- 启用 API -->
|
||||
<div class="wpbridge-settings-row">
|
||||
<div class="wpbridge-settings-info">
|
||||
<h3 class="wpbridge-settings-title"><?php esc_html_e( '启用 Bridge API', 'wpbridge' ); ?></h3>
|
||||
<p class="wpbridge-settings-desc"><?php esc_html_e( '允许通过 REST API 远程访问 WPBridge 功能。', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<label class="wpbridge-toggle">
|
||||
<input type="checkbox" name="api_enabled" value="1" <?php checked( $api_enabled ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 需要认证 -->
|
||||
<div class="wpbridge-settings-row">
|
||||
<div class="wpbridge-settings-info">
|
||||
<h3 class="wpbridge-settings-title"><?php esc_html_e( '需要 API Key 认证', 'wpbridge' ); ?></h3>
|
||||
<p class="wpbridge-settings-desc"><?php esc_html_e( '启用后,所有 API 请求都需要提供有效的 API Key。', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<label class="wpbridge-toggle">
|
||||
<input type="checkbox" name="require_auth" value="1" <?php checked( $require_auth ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
<!-- 需要认证 -->
|
||||
<div class="wpbridge-settings-row">
|
||||
<div class="wpbridge-settings-info">
|
||||
<h3 class="wpbridge-settings-title"><?php esc_html_e( '需要 API Key 认证', 'wpbridge' ); ?></h3>
|
||||
<p class="wpbridge-settings-desc"><?php esc_html_e( '启用后,所有 API 请求都需要提供有效的 API Key。', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<label class="wpbridge-toggle">
|
||||
<input type="checkbox" name="require_auth" value="1" <?php checked( $require_auth ); ?>>
|
||||
<span class="wpbridge-toggle-track"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 速率限制 -->
|
||||
<div class="wpbridge-settings-row">
|
||||
<div class="wpbridge-settings-info">
|
||||
<h3 class="wpbridge-settings-title"><?php esc_html_e( '速率限制', 'wpbridge' ); ?></h3>
|
||||
<p class="wpbridge-settings-desc"><?php esc_html_e( '每个 IP 每分钟允许的最大请求数。', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<input type="number"
|
||||
name="rate_limit"
|
||||
value="<?php echo esc_attr( $rate_limit ); ?>"
|
||||
min="10"
|
||||
max="10000"
|
||||
class="wpbridge-form-input"
|
||||
style="max-width: 100px;">
|
||||
<span style="color: var(--wpbridge-gray-500);"><?php esc_html_e( '次/分钟', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 速率限制 -->
|
||||
<div class="wpbridge-settings-row">
|
||||
<div class="wpbridge-settings-info">
|
||||
<h3 class="wpbridge-settings-title"><?php esc_html_e( '速率限制', 'wpbridge' ); ?></h3>
|
||||
<p class="wpbridge-settings-desc"><?php esc_html_e( '每个 IP 每分钟允许的最大请求数。', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<input type="number"
|
||||
name="rate_limit"
|
||||
value="<?php echo esc_attr( $rate_limit ); ?>"
|
||||
min="10"
|
||||
max="10000"
|
||||
class="wpbridge-form-input"
|
||||
style="max-width: 100px;">
|
||||
<span style="color: var(--wpbridge-gray-500);"><?php esc_html_e( '次/分钟', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-form-actions" style="margin-top: 24px;">
|
||||
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
|
||||
<span class="dashicons dashicons-saved"></span>
|
||||
<?php esc_html_e( '保存设置', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-form-actions" style="margin-top: 24px;">
|
||||
<button type="submit" class="wpbridge-btn wpbridge-btn-primary">
|
||||
<span class="dashicons dashicons-saved"></span>
|
||||
<?php esc_html_e( '保存设置', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- API Keys 管理 -->
|
||||
<div class="wpbridge-settings-panel" style="margin-top: 24px;">
|
||||
<div class="wpbridge-sources-header" style="margin-bottom: 16px;">
|
||||
<h2 class="wpbridge-sources-title"><?php esc_html_e( 'API Keys', 'wpbridge' ); ?></h2>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-generate-api-key">
|
||||
<span class="dashicons dashicons-plus-alt2"></span>
|
||||
<?php esc_html_e( '生成新 Key', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-sources-header" style="margin-bottom: 16px;">
|
||||
<h2 class="wpbridge-sources-title"><?php esc_html_e( 'API Keys', 'wpbridge' ); ?></h2>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-generate-api-key">
|
||||
<span class="dashicons dashicons-plus-alt2"></span>
|
||||
<?php esc_html_e( '生成新 Key', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if ( empty( $api_keys ) ) : ?>
|
||||
<div class="wpbridge-empty" style="padding: 32px;">
|
||||
<span class="dashicons dashicons-admin-network"></span>
|
||||
<h3 class="wpbridge-empty-title"><?php esc_html_e( '暂无 API Key', 'wpbridge' ); ?></h3>
|
||||
<p class="wpbridge-empty-desc"><?php esc_html_e( '生成 API Key 以允许远程访问。', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="wpbridge-api-keys-list">
|
||||
<?php foreach ( $api_keys as $key_data ) : ?>
|
||||
<div class="wpbridge-settings-row" data-key-id="<?php echo esc_attr( $key_data['id'] ?? '' ); ?>">
|
||||
<div class="wpbridge-settings-info">
|
||||
<h3 class="wpbridge-settings-title">
|
||||
<?php echo esc_html( $key_data['name'] ?? __( '未命名', 'wpbridge' ) ); ?>
|
||||
<?php if ( ! empty( $key_data['key_prefix'] ) ) : ?>
|
||||
<code style="margin-left: 8px; font-size: 11px; color: var(--wpbridge-gray-500);">
|
||||
<?php echo esc_html( $key_data['key_prefix'] ); ?>
|
||||
</code>
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
<p class="wpbridge-settings-desc">
|
||||
<?php
|
||||
$created_at = $key_data['created_at'] ?? '';
|
||||
if ( ! empty( $created_at ) ) {
|
||||
// 支持 MySQL 格式和 Unix 时间戳
|
||||
$timestamp = is_numeric( $created_at ) ? $created_at : strtotime( $created_at );
|
||||
/* translators: %s: date */
|
||||
printf(
|
||||
esc_html__( '创建于 %s', 'wpbridge' ),
|
||||
esc_html( date_i18n( get_option( 'date_format' ), $timestamp ) )
|
||||
);
|
||||
}
|
||||
$last_used = $key_data['last_used'] ?? null;
|
||||
if ( ! empty( $last_used ) ) {
|
||||
$last_timestamp = is_numeric( $last_used ) ? $last_used : strtotime( $last_used );
|
||||
/* translators: %s: relative time */
|
||||
printf(
|
||||
' · ' . esc_html__( '最后使用 %s', 'wpbridge' ),
|
||||
esc_html( human_time_diff( $last_timestamp, time() ) . __( '前', 'wpbridge' ) )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-revoke-api-key"
|
||||
data-key-id="<?php echo esc_attr( $key_data['id'] ?? '' ); ?>">
|
||||
<span class="dashicons dashicons-no"></span>
|
||||
<?php esc_html_e( '撤销', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( empty( $api_keys ) ) : ?>
|
||||
<div class="wpbridge-empty" style="padding: 32px;">
|
||||
<span class="dashicons dashicons-admin-network"></span>
|
||||
<h3 class="wpbridge-empty-title"><?php esc_html_e( '暂无 API Key', 'wpbridge' ); ?></h3>
|
||||
<p class="wpbridge-empty-desc"><?php esc_html_e( '生成 API Key 以允许远程访问。', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="wpbridge-api-keys-list">
|
||||
<?php foreach ( $api_keys as $key_data ) : ?>
|
||||
<div class="wpbridge-settings-row" data-key-id="<?php echo esc_attr( $key_data['id'] ?? '' ); ?>">
|
||||
<div class="wpbridge-settings-info">
|
||||
<h3 class="wpbridge-settings-title">
|
||||
<?php echo esc_html( $key_data['name'] ?? __( '未命名', 'wpbridge' ) ); ?>
|
||||
<?php if ( ! empty( $key_data['key_prefix'] ) ) : ?>
|
||||
<code style="margin-left: 8px; font-size: 11px; color: var(--wpbridge-gray-500);">
|
||||
<?php echo esc_html( $key_data['key_prefix'] ); ?>
|
||||
</code>
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
<p class="wpbridge-settings-desc">
|
||||
<?php
|
||||
$created_at = $key_data['created_at'] ?? '';
|
||||
if ( ! empty( $created_at ) ) {
|
||||
// 支持 MySQL 格式和 Unix 时间戳
|
||||
$timestamp = is_numeric( $created_at ) ? $created_at : strtotime( $created_at );
|
||||
/* translators: %s: date */
|
||||
printf(
|
||||
esc_html__( '创建于 %s', 'wpbridge' ),
|
||||
esc_html( date_i18n( get_option( 'date_format' ), $timestamp ) )
|
||||
);
|
||||
}
|
||||
$last_used = $key_data['last_used'] ?? null;
|
||||
if ( ! empty( $last_used ) ) {
|
||||
$last_timestamp = is_numeric( $last_used ) ? $last_used : strtotime( $last_used );
|
||||
/* translators: %s: relative time */
|
||||
printf(
|
||||
' · ' . esc_html__( '最后使用 %s', 'wpbridge' ),
|
||||
esc_html( human_time_diff( $last_timestamp, time() ) . __( '前', 'wpbridge' ) )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-revoke-api-key"
|
||||
data-key-id="<?php echo esc_attr( $key_data['id'] ?? '' ); ?>">
|
||||
<span class="dashicons dashicons-no"></span>
|
||||
<?php esc_html_e( '撤销', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- API 文档 -->
|
||||
<div class="wpbridge-settings-panel" style="margin-top: 24px;">
|
||||
<h2 class="wpbridge-sources-title" style="margin-bottom: 16px;"><?php esc_html_e( 'API 文档', 'wpbridge' ); ?></h2>
|
||||
<h2 class="wpbridge-sources-title" style="margin-bottom: 16px;"><?php esc_html_e( 'API 文档', 'wpbridge' ); ?></h2>
|
||||
|
||||
<div style="font-size: 13px; color: var(--wpbridge-gray-600);">
|
||||
<p><strong><?php esc_html_e( '认证方式', 'wpbridge' ); ?></strong></p>
|
||||
<p><?php esc_html_e( '在请求头中添加 API Key:', 'wpbridge' ); ?></p>
|
||||
<code style="display: block; padding: 12px; background: var(--wpbridge-gray-100); margin: 8px 0;">X-WPBridge-API-Key: your_api_key</code>
|
||||
<div style="font-size: 13px; color: var(--wpbridge-gray-600);">
|
||||
<p><strong><?php esc_html_e( '认证方式', 'wpbridge' ); ?></strong></p>
|
||||
<p><?php esc_html_e( '在请求头中添加 API Key:', 'wpbridge' ); ?></p>
|
||||
<code style="display: block; padding: 12px; background: var(--wpbridge-gray-100); margin: 8px 0;">X-WPBridge-API-Key: your_api_key</code>
|
||||
|
||||
<p style="margin-top: 16px;"><strong><?php esc_html_e( '可用端点', 'wpbridge' ); ?></strong></p>
|
||||
<ul style="margin: 8px 0; padding-left: 20px;">
|
||||
<li><code>GET /wp-json/bridge/v1/status</code> - <?php esc_html_e( 'API 状态', 'wpbridge' ); ?></li>
|
||||
<li><code>GET /wp-json/bridge/v1/sources</code> - <?php esc_html_e( '获取更新源列表', 'wpbridge' ); ?></li>
|
||||
<li><code>GET /wp-json/bridge/v1/check/{source_id}</code> - <?php esc_html_e( '检查更新源状态', 'wpbridge' ); ?></li>
|
||||
<li><code>GET /wp-json/bridge/v1/plugins/{slug}/info</code> - <?php esc_html_e( '获取插件信息', 'wpbridge' ); ?></li>
|
||||
<li><code>GET /wp-json/bridge/v1/themes/{slug}/info</code> - <?php esc_html_e( '获取主题信息', 'wpbridge' ); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p style="margin-top: 16px;"><strong><?php esc_html_e( '可用端点', 'wpbridge' ); ?></strong></p>
|
||||
<ul style="margin: 8px 0; padding-left: 20px;">
|
||||
<li><code>GET /wp-json/bridge/v1/status</code> - <?php esc_html_e( 'API 状态', 'wpbridge' ); ?></li>
|
||||
<li><code>GET /wp-json/bridge/v1/sources</code> - <?php esc_html_e( '获取更新源列表', 'wpbridge' ); ?></li>
|
||||
<li><code>GET /wp-json/bridge/v1/check/{source_id}</code> - <?php esc_html_e( '检查更新源状态', 'wpbridge' ); ?></li>
|
||||
<li><code>GET /wp-json/bridge/v1/plugins/{slug}/info</code> - <?php esc_html_e( '获取插件信息', 'wpbridge' ); ?></li>
|
||||
<li><code>GET /wp-json/bridge/v1/themes/{slug}/info</code> - <?php esc_html_e( '获取主题信息', 'wpbridge' ); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
use WPBridge\UpdateSource\SourceType;
|
||||
|
|
@ -20,340 +20,340 @@ use WPBridge\UpdateSource\SourceType;
|
|||
|
||||
<!-- 诊断工具头部 -->
|
||||
<div class="wpbridge-diagnostics-header">
|
||||
<div class="wpbridge-diagnostics-info">
|
||||
<h2><?php esc_html_e( '诊断工具', 'wpbridge' ); ?></h2>
|
||||
<p><?php esc_html_e( '检查更新源连通性、系统环境和配置状态', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-actions">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-run-all-diagnostics">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
<?php esc_html_e( '运行全部诊断', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-export-diagnostics">
|
||||
<span class="dashicons dashicons-download"></span>
|
||||
<?php esc_html_e( '导出报告', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-info">
|
||||
<h2><?php esc_html_e( '诊断工具', 'wpbridge' ); ?></h2>
|
||||
<p><?php esc_html_e( '检查更新源连通性、系统环境和配置状态', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-actions">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-run-all-diagnostics">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
<?php esc_html_e( '运行全部诊断', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-export-diagnostics">
|
||||
<span class="dashicons dashicons-download"></span>
|
||||
<?php esc_html_e( '导出报告', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 诊断结果概览 -->
|
||||
<div class="wpbridge-diagnostics-summary" style="display: none;" aria-live="polite" aria-atomic="true">
|
||||
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-passed">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
<span class="wpbridge-diagnostics-count">0</span>
|
||||
<span><?php esc_html_e( '通过', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-warnings">
|
||||
<span class="dashicons dashicons-warning"></span>
|
||||
<span class="wpbridge-diagnostics-count">0</span>
|
||||
<span><?php esc_html_e( '警告', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-failed">
|
||||
<span class="dashicons dashicons-dismiss"></span>
|
||||
<span class="wpbridge-diagnostics-count">0</span>
|
||||
<span><?php esc_html_e( '失败', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-passed">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
<span class="wpbridge-diagnostics-count">0</span>
|
||||
<span><?php esc_html_e( '通过', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-warnings">
|
||||
<span class="dashicons dashicons-warning"></span>
|
||||
<span class="wpbridge-diagnostics-count">0</span>
|
||||
<span><?php esc_html_e( '警告', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-summary-item wpbridge-diagnostics-failed">
|
||||
<span class="dashicons dashicons-dismiss"></span>
|
||||
<span class="wpbridge-diagnostics-count">0</span>
|
||||
<span><?php esc_html_e( '失败', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 更新源连通性测试 -->
|
||||
<div class="wpbridge-diagnostics-section">
|
||||
<div class="wpbridge-diagnostics-section-header">
|
||||
<h3>
|
||||
<span class="dashicons dashicons-cloud"></span>
|
||||
<?php esc_html_e( '更新源连通性', 'wpbridge' ); ?>
|
||||
</h3>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-test-all-sources">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '测试全部', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-section-header">
|
||||
<h3>
|
||||
<span class="dashicons dashicons-cloud"></span>
|
||||
<?php esc_html_e( '更新源连通性', 'wpbridge' ); ?>
|
||||
</h3>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-test-all-sources">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '测试全部', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if ( empty( $sources ) ) : ?>
|
||||
<div class="wpbridge-diagnostics-empty">
|
||||
<span class="dashicons dashicons-info-outline"></span>
|
||||
<p><?php esc_html_e( '暂无更新源,请先添加更新源', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="wpbridge-source-tests">
|
||||
<?php foreach ( $sources as $source ) : ?>
|
||||
<div class="wpbridge-source-test-item" data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||
<div class="wpbridge-source-test-info">
|
||||
<div class="wpbridge-source-test-name">
|
||||
<?php echo esc_html( $source->name ?: $source->id ); ?>
|
||||
<?php if ( $source->is_preset ) : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-preset"><?php esc_html_e( '预置', 'wpbridge' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wpbridge-source-test-url"><?php echo esc_html( $source->api_url ); ?></div>
|
||||
</div>
|
||||
<div class="wpbridge-source-test-meta">
|
||||
<span class="wpbridge-badge wpbridge-badge-type <?php echo esc_attr( $source->type ); ?>">
|
||||
<?php echo esc_html( SourceType::get_label( $source->type ) ); ?>
|
||||
</span>
|
||||
<span class="wpbridge-source-test-status">
|
||||
<?php if ( ! $source->enabled ) : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-disabled"><?php esc_html_e( '已禁用', 'wpbridge' ); ?></span>
|
||||
<?php elseif ( isset( $health_status[ $source->id ] ) && is_array( $health_status[ $source->id ] ) ) : ?>
|
||||
<?php
|
||||
$status = $health_status[ $source->id ];
|
||||
$status_class = $status['status'] ?? 'unknown';
|
||||
$status_labels = array(
|
||||
'healthy' => __( '正常', 'wpbridge' ),
|
||||
'degraded' => __( '降级', 'wpbridge' ),
|
||||
'failed' => __( '失败', 'wpbridge' ),
|
||||
);
|
||||
?>
|
||||
<span class="wpbridge-badge wpbridge-badge-status <?php echo esc_attr( $status_class ); ?>">
|
||||
<?php echo esc_html( $status_labels[ $status_class ] ?? $status_class ); ?>
|
||||
</span>
|
||||
<?php if ( ! empty( $status['response_time'] ) ) : ?>
|
||||
<span class="wpbridge-source-test-time"><?php echo esc_html( $status['response_time'] ); ?>ms</span>
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-unknown"><?php esc_html_e( '未测试', 'wpbridge' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="wpbridge-source-test-actions">
|
||||
<button type="button"
|
||||
class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-test-single-source"
|
||||
data-source-id="<?php echo esc_attr( $source->id ); ?>"
|
||||
<?php disabled( ! $source->enabled ); ?>>
|
||||
<span class="dashicons dashicons-admin-site-alt3"></span>
|
||||
<?php esc_html_e( '测试', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( empty( $sources ) ) : ?>
|
||||
<div class="wpbridge-diagnostics-empty">
|
||||
<span class="dashicons dashicons-info-outline"></span>
|
||||
<p><?php esc_html_e( '暂无更新源,请先添加更新源', 'wpbridge' ); ?></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="wpbridge-source-tests">
|
||||
<?php foreach ( $sources as $source ) : ?>
|
||||
<div class="wpbridge-source-test-item" data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||
<div class="wpbridge-source-test-info">
|
||||
<div class="wpbridge-source-test-name">
|
||||
<?php echo esc_html( $source->name ?: $source->id ); ?>
|
||||
<?php if ( $source->is_preset ) : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-preset"><?php esc_html_e( '预置', 'wpbridge' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="wpbridge-source-test-url"><?php echo esc_html( $source->api_url ); ?></div>
|
||||
</div>
|
||||
<div class="wpbridge-source-test-meta">
|
||||
<span class="wpbridge-badge wpbridge-badge-type <?php echo esc_attr( $source->type ); ?>">
|
||||
<?php echo esc_html( SourceType::get_label( $source->type ) ); ?>
|
||||
</span>
|
||||
<span class="wpbridge-source-test-status">
|
||||
<?php if ( ! $source->enabled ) : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-disabled"><?php esc_html_e( '已禁用', 'wpbridge' ); ?></span>
|
||||
<?php elseif ( isset( $health_status[ $source->id ] ) && is_array( $health_status[ $source->id ] ) ) : ?>
|
||||
<?php
|
||||
$status = $health_status[ $source->id ];
|
||||
$status_class = $status['status'] ?? 'unknown';
|
||||
$status_labels = array(
|
||||
'healthy' => __( '正常', 'wpbridge' ),
|
||||
'degraded' => __( '降级', 'wpbridge' ),
|
||||
'failed' => __( '失败', 'wpbridge' ),
|
||||
);
|
||||
?>
|
||||
<span class="wpbridge-badge wpbridge-badge-status <?php echo esc_attr( $status_class ); ?>">
|
||||
<?php echo esc_html( $status_labels[ $status_class ] ?? $status_class ); ?>
|
||||
</span>
|
||||
<?php if ( ! empty( $status['response_time'] ) ) : ?>
|
||||
<span class="wpbridge-source-test-time"><?php echo esc_html( $status['response_time'] ); ?>ms</span>
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
<span class="wpbridge-badge wpbridge-badge-unknown"><?php esc_html_e( '未测试', 'wpbridge' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="wpbridge-source-test-actions">
|
||||
<button type="button"
|
||||
class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-test-single-source"
|
||||
data-source-id="<?php echo esc_attr( $source->id ); ?>"
|
||||
<?php disabled( ! $source->enabled ); ?>>
|
||||
<span class="dashicons dashicons-admin-site-alt3"></span>
|
||||
<?php esc_html_e( '测试', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- 系统环境检查 -->
|
||||
<div class="wpbridge-diagnostics-section">
|
||||
<div class="wpbridge-diagnostics-section-header">
|
||||
<h3>
|
||||
<span class="dashicons dashicons-admin-tools"></span>
|
||||
<?php esc_html_e( '系统环境', 'wpbridge' ); ?>
|
||||
</h3>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-check-environment">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '检查', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-section-header">
|
||||
<h3>
|
||||
<span class="dashicons dashicons-admin-tools"></span>
|
||||
<?php esc_html_e( '系统环境', 'wpbridge' ); ?>
|
||||
</h3>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-check-environment">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '检查', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-environment-checks">
|
||||
<?php
|
||||
// PHP 版本检查
|
||||
$php_version = PHP_VERSION;
|
||||
$php_ok = version_compare( $php_version, '7.4', '>=' );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $php_ok ? 'passed' : 'failed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $php_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( 'PHP 版本', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $php_version ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '要求 >= 7.4', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-environment-checks">
|
||||
<?php
|
||||
// PHP 版本检查
|
||||
$php_version = PHP_VERSION;
|
||||
$php_ok = version_compare( $php_version, '7.4', '>=' );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $php_ok ? 'passed' : 'failed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $php_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( 'PHP 版本', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $php_version ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '要求 >= 7.4', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// WordPress 版本检查
|
||||
$wp_version = get_bloginfo( 'version' );
|
||||
$wp_ok = version_compare( $wp_version, '5.9', '>=' );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $wp_ok ? 'passed' : 'failed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $wp_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( 'WordPress 版本', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $wp_version ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '要求 >= 5.9', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php
|
||||
// WordPress 版本检查
|
||||
$wp_version = get_bloginfo( 'version' );
|
||||
$wp_ok = version_compare( $wp_version, '5.9', '>=' );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $wp_ok ? 'passed' : 'failed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $wp_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( 'WordPress 版本', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $wp_version ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '要求 >= 5.9', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// cURL 扩展检查
|
||||
$curl_ok = function_exists( 'curl_version' );
|
||||
$curl_version = $curl_ok ? curl_version()['version'] : __( '未安装', 'wpbridge' );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $curl_ok ? 'passed' : 'failed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $curl_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( 'cURL 扩展', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $curl_version ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php
|
||||
// cURL 扩展检查
|
||||
$curl_ok = function_exists( 'curl_version' );
|
||||
$curl_version = $curl_ok ? curl_version()['version'] : __( '未安装', 'wpbridge' );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $curl_ok ? 'passed' : 'failed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $curl_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( 'cURL 扩展', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $curl_version ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// OpenSSL 扩展检查
|
||||
$openssl_ok = extension_loaded( 'openssl' );
|
||||
$openssl_version = $openssl_ok ? OPENSSL_VERSION_TEXT : __( '未安装', 'wpbridge' );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $openssl_ok ? 'passed' : 'failed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $openssl_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( 'OpenSSL 扩展', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $openssl_version ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php
|
||||
// OpenSSL 扩展检查
|
||||
$openssl_ok = extension_loaded( 'openssl' );
|
||||
$openssl_version = $openssl_ok ? OPENSSL_VERSION_TEXT : __( '未安装', 'wpbridge' );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $openssl_ok ? 'passed' : 'failed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $openssl_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( 'OpenSSL 扩展', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $openssl_version ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// JSON 扩展检查
|
||||
$json_ok = function_exists( 'json_encode' );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $json_ok ? 'passed' : 'failed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $json_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( 'JSON 扩展', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo $json_ok ? esc_html__( '已安装', 'wpbridge' ) : esc_html__( '未安装', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php
|
||||
// JSON 扩展检查
|
||||
$json_ok = function_exists( 'json_encode' );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $json_ok ? 'passed' : 'failed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $json_ok ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( 'JSON 扩展', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo $json_ok ? esc_html__( '已安装', 'wpbridge' ) : esc_html__( '未安装', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '必需', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 外部 HTTP 请求检查
|
||||
$http_ok = ! defined( 'WP_HTTP_BLOCK_EXTERNAL' ) || ! WP_HTTP_BLOCK_EXTERNAL;
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $http_ok ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $http_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '外部 HTTP 请求', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo $http_ok ? esc_html__( '允许', 'wpbridge' ) : esc_html__( '已阻止', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐允许', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php
|
||||
// 外部 HTTP 请求检查
|
||||
$http_ok = ! defined( 'WP_HTTP_BLOCK_EXTERNAL' ) || ! WP_HTTP_BLOCK_EXTERNAL;
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $http_ok ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $http_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '外部 HTTP 请求', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo $http_ok ? esc_html__( '允许', 'wpbridge' ) : esc_html__( '已阻止', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐允许', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 内存限制检查
|
||||
$memory_limit = ini_get( 'memory_limit' );
|
||||
$memory_bytes = wp_convert_hr_to_bytes( $memory_limit );
|
||||
$memory_ok = $memory_bytes >= 67108864; // 64MB
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $memory_ok ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $memory_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '内存限制', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $memory_limit ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐 >= 64M', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php
|
||||
// 内存限制检查
|
||||
$memory_limit = ini_get( 'memory_limit' );
|
||||
$memory_bytes = wp_convert_hr_to_bytes( $memory_limit );
|
||||
$memory_ok = $memory_bytes >= 67108864; // 64MB
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $memory_ok ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $memory_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '内存限制', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $memory_limit ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐 >= 64M', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 执行时间限制检查
|
||||
$max_execution = ini_get( 'max_execution_time' );
|
||||
$execution_ok = 0 === (int) $max_execution || $max_execution >= 30;
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $execution_ok ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $execution_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '最大执行时间', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $max_execution ); ?>s</span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐 >= 30s', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
// 执行时间限制检查
|
||||
$max_execution = ini_get( 'max_execution_time' );
|
||||
$execution_ok = 0 === (int) $max_execution || $max_execution >= 30;
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $execution_ok ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $execution_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '最大执行时间', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $max_execution ); ?>s</span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '推荐 >= 30s', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置检查 -->
|
||||
<div class="wpbridge-diagnostics-section">
|
||||
<div class="wpbridge-diagnostics-section-header">
|
||||
<h3>
|
||||
<span class="dashicons dashicons-admin-settings"></span>
|
||||
<?php esc_html_e( '配置检查', 'wpbridge' ); ?>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="wpbridge-diagnostics-section-header">
|
||||
<h3>
|
||||
<span class="dashicons dashicons-admin-settings"></span>
|
||||
<?php esc_html_e( '配置检查', 'wpbridge' ); ?>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="wpbridge-config-checks">
|
||||
<?php
|
||||
// 调试模式检查
|
||||
$debug_mode = ! empty( $settings['debug_mode'] );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $debug_mode ? 'warning' : 'passed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $debug_mode ? 'dashicons-warning' : 'dashicons-yes-alt'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '调试模式', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo $debug_mode ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '生产环境建议禁用', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-config-checks">
|
||||
<?php
|
||||
// 调试模式检查
|
||||
$debug_mode = ! empty( $settings['debug_mode'] );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $debug_mode ? 'warning' : 'passed'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $debug_mode ? 'dashicons-warning' : 'dashicons-yes-alt'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '调试模式', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo $debug_mode ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '生产环境建议禁用', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 缓存降级检查
|
||||
$fallback_enabled = ! empty( $settings['fallback_enabled'] );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $fallback_enabled ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $fallback_enabled ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '缓存降级', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo $fallback_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '建议启用', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php
|
||||
// 缓存降级检查
|
||||
$fallback_enabled = ! empty( $settings['fallback_enabled'] );
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $fallback_enabled ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $fallback_enabled ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '缓存降级', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo $fallback_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '建议启用', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 请求超时检查
|
||||
$request_timeout = $settings['request_timeout'] ?? 10;
|
||||
$timeout_ok = $request_timeout >= 5 && $request_timeout <= 30;
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $timeout_ok ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $timeout_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $request_timeout ); ?>s</span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '建议 5-30 秒', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php
|
||||
// 请求超时检查
|
||||
$request_timeout = $settings['request_timeout'] ?? 10;
|
||||
$timeout_ok = $request_timeout >= 5 && $request_timeout <= 30;
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $timeout_ok ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $timeout_ok ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $request_timeout ); ?>s</span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '建议 5-30 秒', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 缓存 TTL 检查
|
||||
$cache_ttl = $settings['cache_ttl'] ?? 43200;
|
||||
$cache_hours = $cache_ttl / 3600;
|
||||
?>
|
||||
<div class="wpbridge-check-item passed">
|
||||
<span class="wpbridge-check-icon dashicons dashicons-yes-alt"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '缓存有效期', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $cache_hours ); ?> <?php esc_html_e( '小时', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '当前设置', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php
|
||||
// 缓存 TTL 检查
|
||||
$cache_ttl = $settings['cache_ttl'] ?? 43200;
|
||||
$cache_hours = $cache_ttl / 3600;
|
||||
?>
|
||||
<div class="wpbridge-check-item passed">
|
||||
<span class="wpbridge-check-icon dashicons dashicons-yes-alt"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '缓存有效期', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $cache_hours ); ?> <?php esc_html_e( '小时', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '当前设置', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 启用的更新源数量检查
|
||||
$enabled_sources = $stats['enabled'] ?? 0;
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $enabled_sources > 0 ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $enabled_sources > 0 ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '启用的更新源', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $enabled_sources ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '至少需要 1 个', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
// 启用的更新源数量检查
|
||||
$enabled_sources = $stats['enabled'] ?? 0;
|
||||
?>
|
||||
<div class="wpbridge-check-item <?php echo $enabled_sources > 0 ? 'passed' : 'warning'; ?>">
|
||||
<span class="wpbridge-check-icon dashicons <?php echo $enabled_sources > 0 ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>"></span>
|
||||
<div class="wpbridge-check-info">
|
||||
<span class="wpbridge-check-label"><?php esc_html_e( '启用的更新源', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-check-value"><?php echo esc_html( $enabled_sources ); ?></span>
|
||||
</div>
|
||||
<span class="wpbridge-check-requirement"><?php esc_html_e( '至少需要 1 个', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 诊断报告导出模态框 -->
|
||||
<div id="wpbridge-export-modal" class="wpbridge-modal" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="wpbridge-export-modal-title">
|
||||
<div class="wpbridge-modal-content">
|
||||
<div class="wpbridge-modal-header">
|
||||
<h3 id="wpbridge-export-modal-title"><?php esc_html_e( '诊断报告', 'wpbridge' ); ?></h3>
|
||||
<button type="button" class="wpbridge-modal-close" aria-label="<?php esc_attr_e( '关闭', 'wpbridge' ); ?>">×</button>
|
||||
</div>
|
||||
<div class="wpbridge-modal-body">
|
||||
<textarea id="wpbridge-diagnostics-report" class="wpbridge-diagnostics-textarea" readonly></textarea>
|
||||
</div>
|
||||
<div class="wpbridge-modal-footer">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-copy-report">
|
||||
<span class="dashicons dashicons-admin-page"></span>
|
||||
<?php esc_html_e( '复制到剪贴板', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-download-report">
|
||||
<span class="dashicons dashicons-download"></span>
|
||||
<?php esc_html_e( '下载报告', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wpbridge-modal-content">
|
||||
<div class="wpbridge-modal-header">
|
||||
<h3 id="wpbridge-export-modal-title"><?php esc_html_e( '诊断报告', 'wpbridge' ); ?></h3>
|
||||
<button type="button" class="wpbridge-modal-close" aria-label="<?php esc_attr_e( '关闭', 'wpbridge' ); ?>">×</button>
|
||||
</div>
|
||||
<div class="wpbridge-modal-body">
|
||||
<textarea id="wpbridge-diagnostics-report" class="wpbridge-diagnostics-textarea" readonly></textarea>
|
||||
</div>
|
||||
<div class="wpbridge-modal-footer">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-copy-report">
|
||||
<span class="dashicons dashicons-admin-page"></span>
|
||||
<?php esc_html_e( '复制到剪贴板', 'wpbridge' ); ?>
|
||||
</button>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-primary wpbridge-download-report">
|
||||
<span class="dashicons dashicons-download"></span>
|
||||
<?php esc_html_e( '下载报告', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,52 +9,52 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wpbridge-logs-panel">
|
||||
<div class="wpbridge-logs-header">
|
||||
<h2 class="wpbridge-logs-title"><?php esc_html_e( '调试日志', 'wpbridge' ); ?></h2>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-clear-logs">
|
||||
<span class="dashicons dashicons-trash"></span>
|
||||
<?php esc_html_e( '清除日志', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-logs-header">
|
||||
<h2 class="wpbridge-logs-title"><?php esc_html_e( '调试日志', 'wpbridge' ); ?></h2>
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-clear-logs">
|
||||
<span class="dashicons dashicons-trash"></span>
|
||||
<?php esc_html_e( '清除日志', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if ( empty( $logs ) ) : ?>
|
||||
<div class="wpbridge-logs-empty">
|
||||
<span class="dashicons dashicons-media-text" style="font-size: 32px; width: 32px; height: 32px; color: var(--wpbridge-gray-300);"></span>
|
||||
<p><?php esc_html_e( '暂无日志记录', 'wpbridge' ); ?></p>
|
||||
<p style="font-size: 12px; color: var(--wpbridge-gray-400);">
|
||||
<?php esc_html_e( '启用调试模式后,日志将显示在这里。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="wpbridge-logs-list">
|
||||
<?php foreach ( array_slice( $logs, 0, 100 ) as $log ) : ?>
|
||||
<div class="wpbridge-log-item">
|
||||
<span class="wpbridge-log-level <?php echo esc_attr( $log['level'] ); ?>">
|
||||
<?php echo esc_html( strtoupper( $log['level'] ) ); ?>
|
||||
</span>
|
||||
<span class="wpbridge-log-time">
|
||||
<?php echo esc_html( $log['time'] ); ?>
|
||||
</span>
|
||||
<span class="wpbridge-log-message">
|
||||
<?php echo esc_html( $log['message'] ); ?>
|
||||
<?php if ( ! empty( $log['context'] ) ) : ?>
|
||||
<code style="display: block; margin-top: 4px; font-size: 11px; color: var(--wpbridge-gray-500);">
|
||||
<?php echo esc_html( wp_json_encode( $log['context'], JSON_UNESCAPED_UNICODE ) ); ?>
|
||||
</code>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( empty( $logs ) ) : ?>
|
||||
<div class="wpbridge-logs-empty">
|
||||
<span class="dashicons dashicons-media-text" style="font-size: 32px; width: 32px; height: 32px; color: var(--wpbridge-gray-300);"></span>
|
||||
<p><?php esc_html_e( '暂无日志记录', 'wpbridge' ); ?></p>
|
||||
<p style="font-size: 12px; color: var(--wpbridge-gray-400);">
|
||||
<?php esc_html_e( '启用调试模式后,日志将显示在这里。', 'wpbridge' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="wpbridge-logs-list">
|
||||
<?php foreach ( array_slice( $logs, 0, 100 ) as $log ) : ?>
|
||||
<div class="wpbridge-log-item">
|
||||
<span class="wpbridge-log-level <?php echo esc_attr( $log['level'] ); ?>">
|
||||
<?php echo esc_html( strtoupper( $log['level'] ) ); ?>
|
||||
</span>
|
||||
<span class="wpbridge-log-time">
|
||||
<?php echo esc_html( $log['time'] ); ?>
|
||||
</span>
|
||||
<span class="wpbridge-log-message">
|
||||
<?php echo esc_html( $log['message'] ); ?>
|
||||
<?php if ( ! empty( $log['context'] ) ) : ?>
|
||||
<code style="display: block; margin-top: 4px; font-size: 11px; color: var(--wpbridge-gray-500);">
|
||||
<?php echo esc_html( wp_json_encode( $log['context'], JSON_UNESCAPED_UNICODE ) ); ?>
|
||||
</code>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16px; padding: 16px; background: var(--wpbridge-info-light); font-size: 13px;">
|
||||
<strong><?php esc_html_e( '提示', 'wpbridge' ); ?>:</strong>
|
||||
<?php esc_html_e( '日志仅在启用调试模式时记录。生产环境建议关闭调试模式以提高性能。', 'wpbridge' ); ?>
|
||||
<strong><?php esc_html_e( '提示', 'wpbridge' ); ?>:</strong>
|
||||
<?php esc_html_e( '日志仅在启用调试模式时记录。生产环境建议关闭调试模式以提高性能。', 'wpbridge' ); ?>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
// 防止直接访问
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
exit;
|
||||
}
|
||||
|
||||
// 计算统计数据
|
||||
|
|
@ -24,13 +24,13 @@ $item_configs = get_option( 'wpbridge_item_sources', array() );
|
|||
$custom_count = 0;
|
||||
$disabled_count = 0;
|
||||
foreach ( $item_configs as $config ) {
|
||||
if ( isset( $config['mode'] ) ) {
|
||||
if ( $config['mode'] === 'custom' ) {
|
||||
++$custom_count;
|
||||
} elseif ( $config['mode'] === 'disabled' ) {
|
||||
++$disabled_count;
|
||||
}
|
||||
}
|
||||
if ( isset( $config['mode'] ) ) {
|
||||
if ( $config['mode'] === 'custom' ) {
|
||||
$custom_count++;
|
||||
} elseif ( $config['mode'] === 'disabled' ) {
|
||||
$disabled_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 健康源统计
|
||||
|
|
@ -38,20 +38,20 @@ $healthy_count = 0;
|
|||
$degraded_count = 0;
|
||||
$failed_count = 0;
|
||||
foreach ( $health_status as $status ) {
|
||||
// 确保 $status 是数组
|
||||
if ( is_array( $status ) && isset( $status['status'] ) ) {
|
||||
switch ( $status['status'] ) {
|
||||
case 'healthy':
|
||||
++$healthy_count;
|
||||
break;
|
||||
case 'degraded':
|
||||
++$degraded_count;
|
||||
break;
|
||||
case 'failed':
|
||||
++$failed_count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 确保 $status 是数组
|
||||
if ( is_array( $status ) && isset( $status['status'] ) ) {
|
||||
switch ( $status['status'] ) {
|
||||
case 'healthy':
|
||||
$healthy_count++;
|
||||
break;
|
||||
case 'degraded':
|
||||
$degraded_count++;
|
||||
break;
|
||||
case 'failed':
|
||||
$failed_count++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存状态
|
||||
|
|
@ -59,214 +59,211 @@ $cache_enabled = ! empty( $settings['fallback_enabled'] );
|
|||
$debug_mode = ! empty( $settings['debug_mode'] );
|
||||
|
||||
// 计算健康百分比
|
||||
$total_checked = count( $health_status );
|
||||
$health_percent = $total_checked > 0 ? round( ( $healthy_count / $total_checked ) * 100 ) : 0;
|
||||
$total_checked = count( $health_status );
|
||||
$health_percent = $total_checked > 0 ? round( ( $healthy_count / $total_checked ) * 100 ) : 0;
|
||||
$health_status_class = $failed_count > 0 ? 'error' : ( $degraded_count > 0 ? 'warning' : 'success' );
|
||||
?>
|
||||
|
||||
<!-- 状态摘要栏 -->
|
||||
<div class="wpbridge-status-bar">
|
||||
<div class="wpbridge-status-bar-item">
|
||||
<span class="wpbridge-status-indicator <?php echo $stats['enabled'] > 0 ? 'active' : 'inactive'; ?>"></span>
|
||||
<span class="wpbridge-status-text">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: number of enabled sources */
|
||||
esc_html__( '%d 个更新源已启用', 'wpbridge' ),
|
||||
$stats['enabled']
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
<?php if ( $debug_mode ) : ?>
|
||||
<div class="wpbridge-status-bar-item wpbridge-status-bar-warning">
|
||||
<span class="dashicons dashicons-warning"></span>
|
||||
<span class="wpbridge-status-text"><?php esc_html_e( '调试模式已开启', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="wpbridge-status-bar-actions">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-sm wpbridge-btn-secondary wpbridge-clear-cache">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '清除缓存', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wpbridge-status-bar-item">
|
||||
<span class="wpbridge-status-indicator <?php echo $stats['enabled'] > 0 ? 'active' : 'inactive'; ?>"></span>
|
||||
<span class="wpbridge-status-text">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: number of enabled sources */
|
||||
esc_html__( '%d 个更新源已启用', 'wpbridge' ),
|
||||
$stats['enabled']
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
<?php if ( $debug_mode ) : ?>
|
||||
<div class="wpbridge-status-bar-item wpbridge-status-bar-warning">
|
||||
<span class="dashicons dashicons-warning"></span>
|
||||
<span class="wpbridge-status-text"><?php esc_html_e( '调试模式已开启', 'wpbridge' ); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="wpbridge-status-bar-actions">
|
||||
<button type="button" class="wpbridge-btn wpbridge-btn-sm wpbridge-btn-secondary wpbridge-clear-cache">
|
||||
<span class="dashicons dashicons-update"></span>
|
||||
<?php esc_html_e( '清除缓存', 'wpbridge' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核心指标 -->
|
||||
<div class="wpbridge-metrics">
|
||||
<!-- 更新源 -->
|
||||
<div class="wpbridge-metric-card">
|
||||
<div class="wpbridge-metric-icon">
|
||||
<span class="dashicons dashicons-cloud"></span>
|
||||
</div>
|
||||
<div class="wpbridge-metric-content">
|
||||
<div class="wpbridge-metric-value"><?php echo esc_html( $stats['total'] ); ?></div>
|
||||
<div class="wpbridge-metric-label"><?php esc_html_e( '更新源', 'wpbridge' ); ?></div>
|
||||
<div class="wpbridge-metric-sub">
|
||||
<span class="wpbridge-metric-highlight"><?php echo esc_html( $stats['enabled'] ); ?></span> <?php esc_html_e( '已启用', 'wpbridge' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#sources" class="wpbridge-metric-link" data-tab-link="sources" aria-label="<?php esc_attr_e( '管理更新源', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-right-alt2"></span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- 更新源 -->
|
||||
<div class="wpbridge-metric-card">
|
||||
<div class="wpbridge-metric-icon">
|
||||
<span class="dashicons dashicons-cloud"></span>
|
||||
</div>
|
||||
<div class="wpbridge-metric-content">
|
||||
<div class="wpbridge-metric-value"><?php echo esc_html( $stats['total'] ); ?></div>
|
||||
<div class="wpbridge-metric-label"><?php esc_html_e( '更新源', 'wpbridge' ); ?></div>
|
||||
<div class="wpbridge-metric-sub">
|
||||
<span class="wpbridge-metric-highlight"><?php echo esc_html( $stats['enabled'] ); ?></span> <?php esc_html_e( '已启用', 'wpbridge' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#sources" class="wpbridge-metric-link" data-tab-link="sources" aria-label="<?php esc_attr_e( '管理更新源', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-right-alt2"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 项目 -->
|
||||
<div class="wpbridge-metric-card">
|
||||
<div class="wpbridge-metric-icon">
|
||||
<span class="dashicons dashicons-admin-plugins"></span>
|
||||
</div>
|
||||
<div class="wpbridge-metric-content">
|
||||
<div class="wpbridge-metric-value"><?php echo esc_html( $plugins_count + $themes_count ); ?></div>
|
||||
<div class="wpbridge-metric-label"><?php esc_html_e( '项目', 'wpbridge' ); ?></div>
|
||||
<div class="wpbridge-metric-sub">
|
||||
<?php echo esc_html( $plugins_count ); ?> <?php esc_html_e( '插件', 'wpbridge' ); ?> / <?php echo esc_html( $themes_count ); ?> <?php esc_html_e( '主题', 'wpbridge' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#projects" class="wpbridge-metric-link" data-tab-link="projects" aria-label="<?php esc_attr_e( '管理项目', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-right-alt2"></span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- 项目 -->
|
||||
<div class="wpbridge-metric-card">
|
||||
<div class="wpbridge-metric-icon">
|
||||
<span class="dashicons dashicons-admin-plugins"></span>
|
||||
</div>
|
||||
<div class="wpbridge-metric-content">
|
||||
<div class="wpbridge-metric-value"><?php echo esc_html( $plugins_count + $themes_count ); ?></div>
|
||||
<div class="wpbridge-metric-label"><?php esc_html_e( '项目', 'wpbridge' ); ?></div>
|
||||
<div class="wpbridge-metric-sub">
|
||||
<?php echo esc_html( $plugins_count ); ?> <?php esc_html_e( '插件', 'wpbridge' ); ?> / <?php echo esc_html( $themes_count ); ?> <?php esc_html_e( '主题', 'wpbridge' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#projects" class="wpbridge-metric-link" data-tab-link="projects" aria-label="<?php esc_attr_e( '管理项目', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-right-alt2"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 自定义配置 -->
|
||||
<div class="wpbridge-metric-card">
|
||||
<div class="wpbridge-metric-icon">
|
||||
<span class="dashicons dashicons-admin-settings"></span>
|
||||
</div>
|
||||
<div class="wpbridge-metric-content">
|
||||
<div class="wpbridge-metric-value"><?php echo esc_html( $custom_count + $disabled_count ); ?></div>
|
||||
<div class="wpbridge-metric-label"><?php esc_html_e( '自定义配置', 'wpbridge' ); ?></div>
|
||||
<div class="wpbridge-metric-sub">
|
||||
<?php if ( $custom_count > 0 || $disabled_count > 0 ) : ?>
|
||||
<?php if ( $custom_count > 0 ) : ?>
|
||||
<span class="wpbridge-metric-highlight"><?php echo esc_html( $custom_count ); ?></span> <?php esc_html_e( '自定义', 'wpbridge' ); ?>
|
||||
<?php endif; ?>
|
||||
<?php if ( $disabled_count > 0 ) : ?>
|
||||
<?php
|
||||
if ( $custom_count > 0 ) :
|
||||
?>
|
||||
/ <?php endif; ?>
|
||||
<span class="wpbridge-metric-muted"><?php echo esc_html( $disabled_count ); ?></span> <?php esc_html_e( '禁用', 'wpbridge' ); ?>
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
<?php esc_html_e( '全部使用默认', 'wpbridge' ); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#projects" class="wpbridge-metric-link" data-tab-link="projects" aria-label="<?php esc_attr_e( '查看配置', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-right-alt2"></span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- 自定义配置 -->
|
||||
<div class="wpbridge-metric-card">
|
||||
<div class="wpbridge-metric-icon">
|
||||
<span class="dashicons dashicons-admin-settings"></span>
|
||||
</div>
|
||||
<div class="wpbridge-metric-content">
|
||||
<div class="wpbridge-metric-value"><?php echo esc_html( $custom_count + $disabled_count ); ?></div>
|
||||
<div class="wpbridge-metric-label"><?php esc_html_e( '自定义配置', 'wpbridge' ); ?></div>
|
||||
<div class="wpbridge-metric-sub">
|
||||
<?php if ( $custom_count > 0 || $disabled_count > 0 ) : ?>
|
||||
<?php if ( $custom_count > 0 ) : ?>
|
||||
<span class="wpbridge-metric-highlight"><?php echo esc_html( $custom_count ); ?></span> <?php esc_html_e( '自定义', 'wpbridge' ); ?>
|
||||
<?php endif; ?>
|
||||
<?php if ( $disabled_count > 0 ) : ?>
|
||||
<?php if ( $custom_count > 0 ) : ?> / <?php endif; ?>
|
||||
<span class="wpbridge-metric-muted"><?php echo esc_html( $disabled_count ); ?></span> <?php esc_html_e( '禁用', 'wpbridge' ); ?>
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
<?php esc_html_e( '全部使用默认', 'wpbridge' ); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#projects" class="wpbridge-metric-link" data-tab-link="projects" aria-label="<?php esc_attr_e( '查看配置', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-right-alt2"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 健康状态 -->
|
||||
<div class="wpbridge-metric-card wpbridge-metric-card-health">
|
||||
<div class="wpbridge-metric-icon wpbridge-metric-icon-<?php echo esc_attr( $health_status_class ); ?>">
|
||||
<span class="dashicons dashicons-heart"></span>
|
||||
</div>
|
||||
<div class="wpbridge-metric-content">
|
||||
<?php if ( $total_checked > 0 ) : ?>
|
||||
<div class="wpbridge-metric-value wpbridge-metric-value-<?php echo esc_attr( $health_status_class ); ?>">
|
||||
<?php echo esc_html( $health_percent ); ?>%
|
||||
</div>
|
||||
<div class="wpbridge-metric-label"><?php esc_html_e( '健康度', 'wpbridge' ); ?></div>
|
||||
<div class="wpbridge-metric-sub">
|
||||
<?php echo esc_html( $healthy_count ); ?>/<?php echo esc_html( $total_checked ); ?> <?php esc_html_e( '源正常', 'wpbridge' ); ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="wpbridge-metric-value wpbridge-metric-value-muted">--</div>
|
||||
<div class="wpbridge-metric-label"><?php esc_html_e( '健康度', 'wpbridge' ); ?></div>
|
||||
<div class="wpbridge-metric-sub"><?php esc_html_e( '暂无检查数据', 'wpbridge' ); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<a href="#diagnostics" class="wpbridge-metric-link" data-tab-link="diagnostics" aria-label="<?php esc_attr_e( '运行诊断', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-right-alt2"></span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- 健康状态 -->
|
||||
<div class="wpbridge-metric-card wpbridge-metric-card-health">
|
||||
<div class="wpbridge-metric-icon wpbridge-metric-icon-<?php echo esc_attr( $health_status_class ); ?>">
|
||||
<span class="dashicons dashicons-heart"></span>
|
||||
</div>
|
||||
<div class="wpbridge-metric-content">
|
||||
<?php if ( $total_checked > 0 ) : ?>
|
||||
<div class="wpbridge-metric-value wpbridge-metric-value-<?php echo esc_attr( $health_status_class ); ?>">
|
||||
<?php echo esc_html( $health_percent ); ?>%
|
||||
</div>
|
||||
<div class="wpbridge-metric-label"><?php esc_html_e( '健康度', 'wpbridge' ); ?></div>
|
||||
<div class="wpbridge-metric-sub">
|
||||
<?php echo esc_html( $healthy_count ); ?>/<?php echo esc_html( $total_checked ); ?> <?php esc_html_e( '源正常', 'wpbridge' ); ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="wpbridge-metric-value wpbridge-metric-value-muted">--</div>
|
||||
<div class="wpbridge-metric-label"><?php esc_html_e( '健康度', 'wpbridge' ); ?></div>
|
||||
<div class="wpbridge-metric-sub"><?php esc_html_e( '暂无检查数据', 'wpbridge' ); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<a href="#diagnostics" class="wpbridge-metric-link" data-tab-link="diagnostics" aria-label="<?php esc_attr_e( '运行诊断', 'wpbridge' ); ?>">
|
||||
<span class="dashicons dashicons-arrow-right-alt2"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速入口 + 系统信息 -->
|
||||
<div class="wpbridge-overview-panels">
|
||||
<!-- 快速入口 -->
|
||||
<div class="wpbridge-panel wpbridge-panel-actions">
|
||||
<div class="wpbridge-panel-header">
|
||||
<h3><?php esc_html_e( '快速入口', 'wpbridge' ); ?></h3>
|
||||
</div>
|
||||
<div class="wpbridge-panel-body">
|
||||
<div class="wpbridge-action-list">
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="wpbridge-action-item">
|
||||
<span class="wpbridge-action-icon">
|
||||
<span class="dashicons dashicons-plus-alt2"></span>
|
||||
</span>
|
||||
<span class="wpbridge-action-text">
|
||||
<span class="wpbridge-action-title"><?php esc_html_e( '添加更新源', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-action-desc"><?php esc_html_e( '配置新的自定义更新源', 'wpbridge' ); ?></span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="#diagnostics" class="wpbridge-action-item" data-tab-link="diagnostics">
|
||||
<span class="wpbridge-action-icon">
|
||||
<span class="dashicons dashicons-admin-tools"></span>
|
||||
</span>
|
||||
<span class="wpbridge-action-text">
|
||||
<span class="wpbridge-action-title"><?php esc_html_e( '诊断工具', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-action-desc"><?php esc_html_e( '检查源连通性和系统环境', 'wpbridge' ); ?></span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="#api" class="wpbridge-action-item" data-tab-link="api">
|
||||
<span class="wpbridge-action-icon">
|
||||
<span class="dashicons dashicons-rest-api"></span>
|
||||
</span>
|
||||
<span class="wpbridge-action-text">
|
||||
<span class="wpbridge-action-title"><?php esc_html_e( 'Bridge API', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-action-desc"><?php esc_html_e( '管理 API 密钥和访问控制', 'wpbridge' ); ?></span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="#settings" class="wpbridge-action-item" data-tab-link="settings">
|
||||
<span class="wpbridge-action-icon">
|
||||
<span class="dashicons dashicons-admin-generic"></span>
|
||||
</span>
|
||||
<span class="wpbridge-action-text">
|
||||
<span class="wpbridge-action-title"><?php esc_html_e( '设置', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-action-desc"><?php esc_html_e( '配置缓存、超时和调试选项', 'wpbridge' ); ?></span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 快速入口 -->
|
||||
<div class="wpbridge-panel wpbridge-panel-actions">
|
||||
<div class="wpbridge-panel-header">
|
||||
<h3><?php esc_html_e( '快速入口', 'wpbridge' ); ?></h3>
|
||||
</div>
|
||||
<div class="wpbridge-panel-body">
|
||||
<div class="wpbridge-action-list">
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="wpbridge-action-item">
|
||||
<span class="wpbridge-action-icon">
|
||||
<span class="dashicons dashicons-plus-alt2"></span>
|
||||
</span>
|
||||
<span class="wpbridge-action-text">
|
||||
<span class="wpbridge-action-title"><?php esc_html_e( '添加更新源', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-action-desc"><?php esc_html_e( '配置新的自定义更新源', 'wpbridge' ); ?></span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="#diagnostics" class="wpbridge-action-item" data-tab-link="diagnostics">
|
||||
<span class="wpbridge-action-icon">
|
||||
<span class="dashicons dashicons-admin-tools"></span>
|
||||
</span>
|
||||
<span class="wpbridge-action-text">
|
||||
<span class="wpbridge-action-title"><?php esc_html_e( '诊断工具', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-action-desc"><?php esc_html_e( '检查源连通性和系统环境', 'wpbridge' ); ?></span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="#api" class="wpbridge-action-item" data-tab-link="api">
|
||||
<span class="wpbridge-action-icon">
|
||||
<span class="dashicons dashicons-rest-api"></span>
|
||||
</span>
|
||||
<span class="wpbridge-action-text">
|
||||
<span class="wpbridge-action-title"><?php esc_html_e( 'Bridge API', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-action-desc"><?php esc_html_e( '管理 API 密钥和访问控制', 'wpbridge' ); ?></span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="#settings" class="wpbridge-action-item" data-tab-link="settings">
|
||||
<span class="wpbridge-action-icon">
|
||||
<span class="dashicons dashicons-admin-generic"></span>
|
||||
</span>
|
||||
<span class="wpbridge-action-text">
|
||||
<span class="wpbridge-action-title"><?php esc_html_e( '设置', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-action-desc"><?php esc_html_e( '配置缓存、超时和调试选项', 'wpbridge' ); ?></span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统信息 -->
|
||||
<div class="wpbridge-panel wpbridge-panel-info">
|
||||
<div class="wpbridge-panel-header">
|
||||
<h3><?php esc_html_e( '系统信息', 'wpbridge' ); ?></h3>
|
||||
</div>
|
||||
<div class="wpbridge-panel-body">
|
||||
<div class="wpbridge-info-list">
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( 'WordPress', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value"><?php echo esc_html( get_bloginfo( 'version' ) ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( 'PHP', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value"><?php echo esc_html( PHP_VERSION ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( '插件版本', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value"><?php echo esc_html( WPBRIDGE_VERSION ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( '缓存 TTL', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value"><?php echo esc_html( ( $settings['cache_ttl'] ?? 43200 ) / 3600 ); ?>h</span>
|
||||
</div>
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value"><?php echo esc_html( $settings['request_timeout'] ?? 10 ); ?>s</span>
|
||||
</div>
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( '缓存降级', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value wpbridge-info-value-<?php echo $cache_enabled ? 'success' : 'muted'; ?>">
|
||||
<?php echo $cache_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 系统信息 -->
|
||||
<div class="wpbridge-panel wpbridge-panel-info">
|
||||
<div class="wpbridge-panel-header">
|
||||
<h3><?php esc_html_e( '系统信息', 'wpbridge' ); ?></h3>
|
||||
</div>
|
||||
<div class="wpbridge-panel-body">
|
||||
<div class="wpbridge-info-list">
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( 'WordPress', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value"><?php echo esc_html( get_bloginfo( 'version' ) ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( 'PHP', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value"><?php echo esc_html( PHP_VERSION ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( '插件版本', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value"><?php echo esc_html( WPBRIDGE_VERSION ); ?></span>
|
||||
</div>
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( '缓存 TTL', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value"><?php echo esc_html( ( $settings['cache_ttl'] ?? 43200 ) / 3600 ); ?>h</span>
|
||||
</div>
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( '请求超时', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value"><?php echo esc_html( $settings['request_timeout'] ?? 10 ); ?>s</span>
|
||||
</div>
|
||||
<div class="wpbridge-info-item">
|
||||
<span class="wpbridge-info-label"><?php esc_html_e( '缓存降级', 'wpbridge' ); ?></span>
|
||||
<span class="wpbridge-info-value wpbridge-info-value-<?php echo $cache_enabled ? 'success' : 'muted'; ?>">
|
||||
<?php echo $cache_enabled ? esc_html__( '已启用', 'wpbridge' ) : esc_html__( '已禁用', 'wpbridge' ); ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue