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 |
102 changed files with 34761 additions and 0 deletions
244
.forgejo/workflows/release.yml
Normal file
244
.forgejo/workflows/release.yml
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
# 文派统一插件发布 CI Workflow
|
||||||
|
# 触发:push tag v*
|
||||||
|
# 运行环境:forgejo-ci-php:latest (Alpine + php-cli + git + rsync + zip + node)
|
||||||
|
name: Release Plugin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
env:
|
||||||
|
PLUGIN_SLUG: ${{ github.event.repository.name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify tools
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
php -v | head -1
|
||||||
|
git --version
|
||||||
|
rsync --version | head -1
|
||||||
|
zip --version | head -2
|
||||||
|
jq --version
|
||||||
|
curl --version | head -1
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${GITHUB_REF#refs/tags/}"
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Tag: $TAG, Version: $VERSION"
|
||||||
|
|
||||||
|
- name: Detect main plugin file
|
||||||
|
id: detect
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
MAIN_FILE=$(grep -rl "Plugin Name:" *.php 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MAIN_FILE" ]; then
|
||||||
|
echo "::error::No main plugin file found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "main_file=$MAIN_FILE" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Main file: $MAIN_FILE"
|
||||||
|
|
||||||
|
- name: Validate version consistency
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
MAIN_FILE="${{ steps.detect.outputs.main_file }}"
|
||||||
|
HEADER_VERSION=$(grep -i "Version:" "$MAIN_FILE" | grep -v "Requires" | grep -v "Tested" | head -1 | sed "s/.*Version:[[:space:]]*//" | sed "s/[[:space:]]*$//" | tr -d "\r")
|
||||||
|
echo "Extracted header version: [$HEADER_VERSION]"
|
||||||
|
if [ "$HEADER_VERSION" != "$VERSION" ]; then
|
||||||
|
echo "::error::Version mismatch: tag=$VERSION, header=$HEADER_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -f "readme.txt" ]; then
|
||||||
|
STABLE_TAG=$(grep -i "^Stable tag:" readme.txt | head -1 | sed "s/.*Stable tag:[[:space:]]*//" | sed "s/[[:space:]]*$//" | tr -d "\r")
|
||||||
|
if [ -n "$STABLE_TAG" ] && [ "$STABLE_TAG" != "$VERSION" ]; then
|
||||||
|
echo "::error::Stable tag mismatch: tag=$VERSION, readme=$STABLE_TAG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "Version consistency check passed: $VERSION"
|
||||||
|
|
||||||
|
- name: PHP Lint
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r file; do
|
||||||
|
if ! php -l "$file" > /dev/null 2>&1; then
|
||||||
|
php -l "$file"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*")
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::PHP lint found $ERRORS error(s)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "PHP lint passed"
|
||||||
|
|
||||||
|
- name: Build ZIP
|
||||||
|
id: build
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
SLUG="${{ env.PLUGIN_SLUG }}"
|
||||||
|
ZIP_NAME="${SLUG}-${VERSION}.zip"
|
||||||
|
BUILD_DIR="/tmp/build/${SLUG}"
|
||||||
|
RELEASE_DIR="/tmp/release"
|
||||||
|
|
||||||
|
rm -rf "$BUILD_DIR" "$RELEASE_DIR"
|
||||||
|
mkdir -p "$BUILD_DIR" "$RELEASE_DIR"
|
||||||
|
|
||||||
|
rsync -a \
|
||||||
|
--exclude=".git" \
|
||||||
|
--exclude=".github" \
|
||||||
|
--exclude=".forgejo" \
|
||||||
|
--exclude=".gitignore" \
|
||||||
|
--exclude=".gitattributes" \
|
||||||
|
--exclude=".editorconfig" \
|
||||||
|
--exclude=".env*" \
|
||||||
|
--exclude="node_modules" \
|
||||||
|
--exclude="tests" \
|
||||||
|
--exclude="phpunit.xml*" \
|
||||||
|
--exclude="phpcs.xml*" \
|
||||||
|
--exclude="phpstan.neon*" \
|
||||||
|
--exclude="composer.json" \
|
||||||
|
--exclude="composer.lock" \
|
||||||
|
--exclude="package.json" \
|
||||||
|
--exclude="package-lock.json" \
|
||||||
|
--exclude="Gruntfile.js" \
|
||||||
|
--exclude="webpack.config.js" \
|
||||||
|
--exclude="*.md" \
|
||||||
|
--exclude="LICENSE" \
|
||||||
|
--exclude="Makefile" \
|
||||||
|
./ "$BUILD_DIR/"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd /tmp/build
|
||||||
|
zip -qr "${RELEASE_DIR}/${ZIP_NAME}" "${SLUG}/"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "zip_path=${RELEASE_DIR}/${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_dir=${RELEASE_DIR}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Built: ${ZIP_NAME} ($(du -h "${RELEASE_DIR}/${ZIP_NAME}" | cut -f1))"
|
||||||
|
|
||||||
|
- name: Calculate SHA-256
|
||||||
|
id: checksum
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ZIP_PATH="${{ steps.build.outputs.zip_path }}"
|
||||||
|
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
|
||||||
|
RELEASE_DIR="${{ steps.build.outputs.release_dir }}"
|
||||||
|
SHA256=$(sha256sum "$ZIP_PATH" | cut -d" " -f1)
|
||||||
|
echo "$SHA256 $ZIP_NAME" > "${RELEASE_DIR}/${ZIP_NAME}.sha256"
|
||||||
|
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "SHA-256: $SHA256"
|
||||||
|
ls -la "$RELEASE_DIR"
|
||||||
|
|
||||||
|
- name: Create or Update Release & Upload Assets
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
AUTH_TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||||
|
if [ -z "$AUTH_TOKEN" ]; then
|
||||||
|
echo "::error::Missing auth token: set RELEASE_TOKEN or use default GITHUB_TOKEN"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAG="${{ steps.version.outputs.tag }}"
|
||||||
|
SLUG="${{ env.PLUGIN_SLUG }}"
|
||||||
|
RELEASE_DIR="${{ steps.build.outputs.release_dir }}"
|
||||||
|
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
|
||||||
|
SHA256="${{ steps.checksum.outputs.sha256 }}"
|
||||||
|
API_URL="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
|
AUTH_HEADER="Authorization: token ${AUTH_TOKEN}"
|
||||||
|
printf -v RELEASE_NOTES '## %s %s\n\n### Checksums\n\n| File | SHA-256 |\n|------|---------|\\n| %s | %s |\n' "$SLUG" "$TAG" "$ZIP_NAME" "$SHA256"
|
||||||
|
|
||||||
|
echo ">>> Resolving release ${TAG}"
|
||||||
|
STATUS=$(curl -sS -o /tmp/release.json -w "%{http_code}" \
|
||||||
|
-H "$AUTH_HEADER" \
|
||||||
|
"${API_URL}/releases/tags/${TAG}")
|
||||||
|
|
||||||
|
if [ "$STATUS" = "200" ]; then
|
||||||
|
RELEASE_ID=$(jq -r '.id' /tmp/release.json)
|
||||||
|
echo "Release exists (id=${RELEASE_ID}), patching metadata"
|
||||||
|
curl -sS -f -X PATCH \
|
||||||
|
-H "$AUTH_HEADER" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(jq -n --arg name "$TAG" --arg body "$RELEASE_NOTES" '{name: $name, body: $body, draft: false, prerelease: false}')" \
|
||||||
|
"${API_URL}/releases/${RELEASE_ID}" > /tmp/release.json
|
||||||
|
elif [ "$STATUS" = "404" ]; then
|
||||||
|
echo "Release not found, creating"
|
||||||
|
curl -sS -f -X POST \
|
||||||
|
-H "$AUTH_HEADER" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(jq -n --arg tag "$TAG" --arg name "$TAG" --arg body "$RELEASE_NOTES" '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')" \
|
||||||
|
"${API_URL}/releases" > /tmp/release.json
|
||||||
|
else
|
||||||
|
echo "::error::Failed to query release (HTTP ${STATUS})"
|
||||||
|
cat /tmp/release.json
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_ID=$(jq -r '.id' /tmp/release.json)
|
||||||
|
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
||||||
|
echo "::error::Failed to resolve release id"
|
||||||
|
cat /tmp/release.json
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ">>> Uploading assets to release ${RELEASE_ID}"
|
||||||
|
for FILE in "${RELEASE_DIR}"/*; do
|
||||||
|
FILENAME=$(basename "$FILE")
|
||||||
|
EXISTING_ASSET_ID=$(jq -r --arg n "$FILENAME" '.assets[]? | select(.name == $n) | .id' /tmp/release.json | head -1)
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_ASSET_ID" ] && [ "$EXISTING_ASSET_ID" != "null" ]; then
|
||||||
|
echo " deleting old asset: ${FILENAME} (id=${EXISTING_ASSET_ID})"
|
||||||
|
curl -sS -f -X DELETE \
|
||||||
|
-H "$AUTH_HEADER" \
|
||||||
|
"${API_URL}/releases/${RELEASE_ID}/assets/${EXISTING_ASSET_ID}" > /dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENCODED_NAME=$(printf "%s" "$FILENAME" | jq -sRr @uri)
|
||||||
|
echo " uploading: ${FILENAME}"
|
||||||
|
curl -sS -f --retry 3 --retry-delay 2 --retry-all-errors \
|
||||||
|
-X POST \
|
||||||
|
-H "$AUTH_HEADER" \
|
||||||
|
-F "attachment=@${FILE}" \
|
||||||
|
"${API_URL}/releases/${RELEASE_ID}/assets?name=${ENCODED_NAME}" > /dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ">>> Verifying uploaded assets"
|
||||||
|
curl -sS -f -H "$AUTH_HEADER" "${API_URL}/releases/${RELEASE_ID}" > /tmp/release-final.json
|
||||||
|
for FILE in "${RELEASE_DIR}"/*; do
|
||||||
|
FILENAME=$(basename "$FILE")
|
||||||
|
if ! jq -e --arg n "$FILENAME" '.assets[]? | select(.name == $n)' /tmp/release-final.json > /dev/null; then
|
||||||
|
echo "::error::Missing uploaded asset: ${FILENAME}"
|
||||||
|
jq -r '.assets[]?.name' /tmp/release-final.json
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ">>> Release ${TAG} published successfully"
|
||||||
|
echo "Release URL: ${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
|
||||||
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/
|
||||||
498
ARCHITECTURE.md
Normal file
498
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,498 @@
|
||||||
|
# WPBridge 业务流程与架构设计
|
||||||
|
|
||||||
|
> 详细的业务流程图和系统架构
|
||||||
|
|
||||||
|
*创建日期: 2026-02-04*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 核心业务流程
|
||||||
|
|
||||||
|
### 1.1 更新源桥接流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ WordPress 更新检查流程 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
WordPress 核心 WPBridge 外部源
|
||||||
|
│ │ │
|
||||||
|
│ 1. 触发更新检查 │ │
|
||||||
|
│ (wp_update_plugins) │ │
|
||||||
|
│──────────────────────────────>│ │
|
||||||
|
│ │ │
|
||||||
|
│ │ 2. 检查是否有自定义源 │
|
||||||
|
│ │ (查询 wpbridge_sources) │
|
||||||
|
│ │ │
|
||||||
|
│ │ 3. 遍历匹配的源 │
|
||||||
|
│ │──────────────────────────────>
|
||||||
|
│ │ │
|
||||||
|
│ │ 4. 获取版本信息 │
|
||||||
|
│ │<──────────────────────────────
|
||||||
|
│ │ │
|
||||||
|
│ │ 5. 缓存结果 │
|
||||||
|
│ │ (transient) │
|
||||||
|
│ │ │
|
||||||
|
│ 6. 返回更新信息 │ │
|
||||||
|
│<──────────────────────────────│ │
|
||||||
|
│ │ │
|
||||||
|
│ 7. 显示更新通知 │ │
|
||||||
|
│ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 更新下载流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 插件/主题下载流程 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
用户点击更新 WPBridge 外部源
|
||||||
|
│ │ │
|
||||||
|
│ 1. 触发下载 │ │
|
||||||
|
│ (upgrader_pre_download) │ │
|
||||||
|
│──────────────────────────────>│ │
|
||||||
|
│ │ │
|
||||||
|
│ │ 2. 检查是否需要桥接 │
|
||||||
|
│ │ (匹配 slug) │
|
||||||
|
│ │ │
|
||||||
|
│ │ 3. 获取下载 URL │
|
||||||
|
│ │ (可能需要认证) │
|
||||||
|
│ │──────────────────────────────>
|
||||||
|
│ │ │
|
||||||
|
│ │ 4. 下载 ZIP 包 │
|
||||||
|
│ │<──────────────────────────────
|
||||||
|
│ │ │
|
||||||
|
│ │ 5. 安全检查 │
|
||||||
|
│ │ (哈希/大小/结构) │
|
||||||
|
│ │ │
|
||||||
|
│ 6. 返回本地文件路径 │ │
|
||||||
|
│<──────────────────────────────│ │
|
||||||
|
│ │ │
|
||||||
|
│ 7. WordPress 安装更新 │ │
|
||||||
|
│ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 AI 桥接流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ AI 请求桥接流程 │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
第三方插件 WPBridge AI 服务
|
||||||
|
(如 AI Engine) │ │
|
||||||
|
│ │ │
|
||||||
|
│ 1. 发起 HTTP 请求 │ │
|
||||||
|
│ (api.openai.com) │ │
|
||||||
|
│──────────────────────────────>│ │
|
||||||
|
│ │ │
|
||||||
|
│ │ 2. 白名单检查 │
|
||||||
|
│ │ (是否在拦截列表) │
|
||||||
|
│ │ │
|
||||||
|
│ │ 3. 模式判断 │
|
||||||
|
│ │ ┌─────────────────────────┐│
|
||||||
|
│ │ │ MODE_DISABLED → 放行 ││
|
||||||
|
│ │ │ MODE_PASSTHROUGH → 转发 ││
|
||||||
|
│ │ │ MODE_WPMIND → WPMind ││
|
||||||
|
│ │ └─────────────────────────┘│
|
||||||
|
│ │ │
|
||||||
|
│ │ 4a. 透传模式 │
|
||||||
|
│ │──────────────────────────────>
|
||||||
|
│ │ (用户指定端点) │
|
||||||
|
│ │ │
|
||||||
|
│ │ 4b. WPMind 模式 │
|
||||||
|
│ │──────> WPMind ──────────────>
|
||||||
|
│ │ (国内 AI 服务) │
|
||||||
|
│ │ │
|
||||||
|
│ 5. 返回响应 │ │
|
||||||
|
│<──────────────────────────────│ │
|
||||||
|
│ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 系统架构
|
||||||
|
|
||||||
|
### 2.1 整体架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ WPBridge 架构 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Admin Layer │ │
|
||||||
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||||
|
│ │ │ AdminPage │ │ SourceEditor│ │ AISettings │ │ │
|
||||||
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Core Layer │ │
|
||||||
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||||
|
│ │ │ Plugin │ │ Settings │ │ Logger │ │ │
|
||||||
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────────────────┴──────────────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
|
||||||
|
│ │ │ UpdateSource │ │ AIBridge │ │ │
|
||||||
|
│ │ │ Module │ │ Module │ │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │
|
||||||
|
│ │ │ │SourceManager │ │ │ │ Interceptor │ │ │ │
|
||||||
|
│ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │
|
||||||
|
│ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │
|
||||||
|
│ │ │ │PluginUpdater │ │ │ │ WPMindBridge │ │ │ │
|
||||||
|
│ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │
|
||||||
|
│ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │ │
|
||||||
|
│ │ │ │ ThemeUpdater │ │ │ │ Passthrough │ │ │ │
|
||||||
|
│ │ │ └───────────────┘ │ │ └───────────────┘ │ │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ └─────────────────────┘ └─────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Handler Layer │ │
|
||||||
|
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||||||
|
│ │ │ JSON │ │ GitHub │ │ GitLab │ │ WenPai │ │ │
|
||||||
|
│ │ │ Handler │ │ Handler │ │ Handler │ │ Handler │ │ │
|
||||||
|
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
wpbridge/
|
||||||
|
├── wpbridge.php # 主文件
|
||||||
|
├── uninstall.php # 卸载脚本
|
||||||
|
├── CHANGELOG.md # 更新日志
|
||||||
|
│
|
||||||
|
├── includes/
|
||||||
|
│ ├── Core/
|
||||||
|
│ │ ├── Plugin.php # 插件主类
|
||||||
|
│ │ ├── Loader.php # 自动加载
|
||||||
|
│ │ ├── Settings.php # 设置管理
|
||||||
|
│ │ ├── Logger.php # 日志系统
|
||||||
|
│ │ └── Encryption.php # 加密工具
|
||||||
|
│ │
|
||||||
|
│ ├── UpdateSource/
|
||||||
|
│ │ ├── SourceManager.php # 更新源管理
|
||||||
|
│ │ ├── SourceModel.php # 数据模型
|
||||||
|
│ │ ├── PluginUpdater.php # 插件更新器
|
||||||
|
│ │ ├── ThemeUpdater.php # 主题更新器
|
||||||
|
│ │ ├── CacheManager.php # 缓存管理
|
||||||
|
│ │ └── Handlers/
|
||||||
|
│ │ ├── HandlerInterface.php # 统一接口
|
||||||
|
│ │ ├── JsonHandler.php # JSON API
|
||||||
|
│ │ ├── GitHubHandler.php # GitHub
|
||||||
|
│ │ ├── GitLabHandler.php # GitLab
|
||||||
|
│ │ └── WenPaiGitHandler.php # 菲码源库
|
||||||
|
│ │
|
||||||
|
│ ├── AIBridge/
|
||||||
|
│ │ ├── AIGateway.php # AI 网关
|
||||||
|
│ │ ├── Interceptor.php # 请求拦截
|
||||||
|
│ │ ├── WPMindBridge.php # WPMind 桥接
|
||||||
|
│ │ ├── Passthrough.php # 透传模式
|
||||||
|
│ │ └── Adapters/
|
||||||
|
│ │ ├── AdapterInterface.php
|
||||||
|
│ │ ├── YoastAdapter.php
|
||||||
|
│ │ └── RankMathAdapter.php
|
||||||
|
│ │
|
||||||
|
│ └── Admin/
|
||||||
|
│ ├── AdminPage.php # 管理页面
|
||||||
|
│ ├── SourceEditor.php # 更新源编辑器
|
||||||
|
│ └── AISettings.php # AI 设置
|
||||||
|
│
|
||||||
|
├── templates/
|
||||||
|
│ └── admin/
|
||||||
|
│ ├── settings.php
|
||||||
|
│ ├── source-list.php
|
||||||
|
│ ├── source-editor.php
|
||||||
|
│ └── ai-settings.php
|
||||||
|
│
|
||||||
|
├── assets/
|
||||||
|
│ ├── css/
|
||||||
|
│ │ └── admin.css
|
||||||
|
│ └── js/
|
||||||
|
│ └── admin.js
|
||||||
|
│
|
||||||
|
└── languages/
|
||||||
|
└── wpbridge.pot
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 数据模型
|
||||||
|
|
||||||
|
### 3.1 更新源数据结构
|
||||||
|
|
||||||
|
```php
|
||||||
|
// wp_options: wpbridge_sources
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'id' => 'src_abc123', // 唯一标识
|
||||||
|
'name' => 'My Plugin Source', // 显示名称
|
||||||
|
'type' => 'json', // json|github|gitlab|wenpai|zip
|
||||||
|
'slug' => 'my-plugin', // 插件/主题 slug
|
||||||
|
'item_type' => 'plugin', // plugin|theme
|
||||||
|
'source_url' => 'https://...', // 更新源地址
|
||||||
|
'auth_type' => 'token', // none|token|basic|oauth
|
||||||
|
'auth_token' => 'encrypted:...', // 加密存储
|
||||||
|
'branch' => 'main', // Git 分支(可选)
|
||||||
|
'enabled' => true, // 是否启用
|
||||||
|
'priority' => 10, // 优先级
|
||||||
|
'created_at' => '2026-02-04 10:00:00',
|
||||||
|
'updated_at' => '2026-02-04 10:00:00',
|
||||||
|
],
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 缓存数据结构
|
||||||
|
|
||||||
|
```php
|
||||||
|
// transient: wpbridge_update_cache_{slug}
|
||||||
|
[
|
||||||
|
'version' => '2.0.0',
|
||||||
|
'download_url' => 'https://...',
|
||||||
|
'tested' => '6.4',
|
||||||
|
'requires' => '5.9',
|
||||||
|
'requires_php' => '7.4',
|
||||||
|
'last_checked' => 1707012000,
|
||||||
|
'source_id' => 'src_abc123',
|
||||||
|
]
|
||||||
|
|
||||||
|
// transient: wpbridge_source_health_{source_id}
|
||||||
|
[
|
||||||
|
'status' => 'healthy', // healthy|degraded|failed
|
||||||
|
'last_check' => 1707012000,
|
||||||
|
'error_count' => 0,
|
||||||
|
'last_error' => null,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 AI 桥接配置
|
||||||
|
|
||||||
|
```php
|
||||||
|
// wp_options: wpbridge_ai_settings
|
||||||
|
[
|
||||||
|
'enabled' => true,
|
||||||
|
'mode' => 'wpmind', // disabled|passthrough|wpmind
|
||||||
|
'custom_endpoint' => '', // 透传模式的目标端点
|
||||||
|
'whitelist' => [ // 拦截白名单
|
||||||
|
'api.openai.com',
|
||||||
|
'api.anthropic.com',
|
||||||
|
],
|
||||||
|
'adapters' => [ // 启用的适配器
|
||||||
|
'yoast' => true,
|
||||||
|
'rankmath' => false,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 核心类设计
|
||||||
|
|
||||||
|
### 4.1 SourceHandlerInterface
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
interface SourceHandlerInterface {
|
||||||
|
/**
|
||||||
|
* 获取处理器能力
|
||||||
|
* @return array ['auth' => [], 'version' => [], 'download' => []]
|
||||||
|
*/
|
||||||
|
public function getCapabilities(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最新版本信息
|
||||||
|
* @param string $identifier 源标识(URL/仓库地址)
|
||||||
|
* @param array $options 选项(认证信息等)
|
||||||
|
* @return VersionInfo|null
|
||||||
|
*/
|
||||||
|
public function getLatestVersion(string $identifier, array $options = []): ?VersionInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载 URL
|
||||||
|
* @param string $identifier 源标识
|
||||||
|
* @param string $version 版本号
|
||||||
|
* @param array $options 选项
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getDownloadUrl(string $identifier, string $version, array $options = []): ?string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证源可用性
|
||||||
|
* @param string $identifier 源标识
|
||||||
|
* @param array $options 选项
|
||||||
|
* @return HealthStatus
|
||||||
|
*/
|
||||||
|
public function checkHealth(string $identifier, array $options = []): HealthStatus;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 VersionInfo 值对象
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace WPBridge\UpdateSource;
|
||||||
|
|
||||||
|
class VersionInfo {
|
||||||
|
public string $version;
|
||||||
|
public string $downloadUrl;
|
||||||
|
public ?string $tested = null;
|
||||||
|
public ?string $requires = null;
|
||||||
|
public ?string $requiresPhp = null;
|
||||||
|
public ?string $changelog = null;
|
||||||
|
public ?string $hash = null;
|
||||||
|
public ?int $fileSize = null;
|
||||||
|
public int $checkedAt;
|
||||||
|
|
||||||
|
public function __construct(string $version, string $downloadUrl) {
|
||||||
|
$this->version = $version;
|
||||||
|
$this->downloadUrl = $downloadUrl;
|
||||||
|
$this->checkedAt = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array {
|
||||||
|
return [
|
||||||
|
'version' => $this->version,
|
||||||
|
'download_url' => $this->downloadUrl,
|
||||||
|
'tested' => $this->tested,
|
||||||
|
'requires' => $this->requires,
|
||||||
|
'requires_php' => $this->requiresPhp,
|
||||||
|
'changelog' => $this->changelog,
|
||||||
|
'hash' => $this->hash,
|
||||||
|
'file_size' => $this->fileSize,
|
||||||
|
'checked_at' => $this->checkedAt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 HealthStatus 值对象
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
namespace WPBridge\UpdateSource;
|
||||||
|
|
||||||
|
class HealthStatus {
|
||||||
|
const STATUS_HEALTHY = 'healthy';
|
||||||
|
const STATUS_DEGRADED = 'degraded';
|
||||||
|
const STATUS_FAILED = 'failed';
|
||||||
|
|
||||||
|
public string $status;
|
||||||
|
public int $responseTime; // ms
|
||||||
|
public ?string $error = null;
|
||||||
|
public int $checkedAt;
|
||||||
|
|
||||||
|
public static function healthy(int $responseTime): self {
|
||||||
|
$status = new self();
|
||||||
|
$status->status = self::STATUS_HEALTHY;
|
||||||
|
$status->responseTime = $responseTime;
|
||||||
|
$status->checkedAt = time();
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function failed(string $error): self {
|
||||||
|
$status = new self();
|
||||||
|
$status->status = self::STATUS_FAILED;
|
||||||
|
$status->responseTime = 0;
|
||||||
|
$status->error = $error;
|
||||||
|
$status->checkedAt = time();
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. WordPress 钩子集成
|
||||||
|
|
||||||
|
### 5.1 更新检查钩子
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 插件更新检查
|
||||||
|
add_filter('pre_set_site_transient_update_plugins', [$this, 'checkPluginUpdates'], 10, 1);
|
||||||
|
|
||||||
|
// 主题更新检查
|
||||||
|
add_filter('pre_set_site_transient_update_themes', [$this, 'checkThemeUpdates'], 10, 1);
|
||||||
|
|
||||||
|
// 插件信息 API
|
||||||
|
add_filter('plugins_api', [$this, 'pluginInfo'], 20, 3);
|
||||||
|
|
||||||
|
// 主题信息 API
|
||||||
|
add_filter('themes_api', [$this, 'themeInfo'], 20, 3);
|
||||||
|
|
||||||
|
// 下载包过滤
|
||||||
|
add_filter('upgrader_pre_download', [$this, 'filterDownload'], 10, 3);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 AI 拦截钩子
|
||||||
|
|
||||||
|
```php
|
||||||
|
// HTTP 请求拦截(优先级 1,最早执行)
|
||||||
|
add_filter('pre_http_request', [$this, 'interceptAIRequest'], 1, 3);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 错误处理与日志
|
||||||
|
|
||||||
|
### 6.1 错误码定义
|
||||||
|
|
||||||
|
```php
|
||||||
|
class ErrorCodes {
|
||||||
|
// 源相关错误 (1xxx)
|
||||||
|
const SOURCE_NOT_FOUND = 1001;
|
||||||
|
const SOURCE_UNREACHABLE = 1002;
|
||||||
|
const SOURCE_INVALID_RESPONSE = 1003;
|
||||||
|
const SOURCE_AUTH_FAILED = 1004;
|
||||||
|
|
||||||
|
// 下载相关错误 (2xxx)
|
||||||
|
const DOWNLOAD_FAILED = 2001;
|
||||||
|
const DOWNLOAD_HASH_MISMATCH = 2002;
|
||||||
|
const DOWNLOAD_SIZE_EXCEEDED = 2003;
|
||||||
|
const DOWNLOAD_INVALID_ZIP = 2004;
|
||||||
|
|
||||||
|
// AI 桥接错误 (3xxx)
|
||||||
|
const AI_WPMIND_UNAVAILABLE = 3001;
|
||||||
|
const AI_ENDPOINT_UNREACHABLE = 3002;
|
||||||
|
const AI_RESPONSE_INVALID = 3003;
|
||||||
|
|
||||||
|
// 配置错误 (4xxx)
|
||||||
|
const CONFIG_INVALID = 4001;
|
||||||
|
const CONFIG_ENCRYPTION_FAILED = 4002;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 日志级别
|
||||||
|
|
||||||
|
```php
|
||||||
|
class Logger {
|
||||||
|
const LEVEL_DEBUG = 'debug';
|
||||||
|
const LEVEL_INFO = 'info';
|
||||||
|
const LEVEL_WARNING = 'warning';
|
||||||
|
const LEVEL_ERROR = 'error';
|
||||||
|
|
||||||
|
public function log(string $level, string $message, array $context = []): void;
|
||||||
|
public function debug(string $message, array $context = []): void;
|
||||||
|
public function info(string $message, array $context = []): void;
|
||||||
|
public function warning(string $message, array $context = []): void;
|
||||||
|
public function error(string $message, array $context = []): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*最后更新: 2026-02-04*
|
||||||
417
DEVELOPMENT-PLAN.md
Normal file
417
DEVELOPMENT-PLAN.md
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
# WPBridge 开发计划
|
||||||
|
|
||||||
|
> 完整的开发计划和任务分解
|
||||||
|
|
||||||
|
*创建日期: 2026-02-04*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、项目概述
|
||||||
|
|
||||||
|
### 1.1 项目定位
|
||||||
|
|
||||||
|
**WPBridge(文派云桥)** - 自定义源桥接器,让用户完全控制 WordPress 的外部连接。
|
||||||
|
|
||||||
|
### 1.2 核心价值
|
||||||
|
|
||||||
|
1. **自定义更新源桥接** - 支持自托管更新服务器、商业插件更新源
|
||||||
|
2. **性能优化** - 并行请求、智能缓存、减少后台加载时间
|
||||||
|
3. **AI 服务桥接** - OpenAI API 兼容层,可选依赖 WPMind
|
||||||
|
|
||||||
|
### 1.3 非目标
|
||||||
|
|
||||||
|
- 不替代 WordPress.org 官方源(由文派叶子 WPCY 负责)
|
||||||
|
- 不提供镜像/CDN 服务
|
||||||
|
- 不破解/绕过商业插件授权
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、版本规划
|
||||||
|
|
||||||
|
```
|
||||||
|
v0.1.0 MVP(2-3 周)
|
||||||
|
↓
|
||||||
|
v0.2.0 性能优化 + Git 支持(2-3 周)
|
||||||
|
↓
|
||||||
|
v0.3.0 AI 桥接 + 商业插件(2-3 周)
|
||||||
|
↓
|
||||||
|
v0.4.0 Cloud API(可选,1-2 周)
|
||||||
|
↓
|
||||||
|
v1.0.0 正式发布(1-2 周)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、v0.1.0 MVP 详细计划
|
||||||
|
|
||||||
|
### 3.1 目标
|
||||||
|
|
||||||
|
实现最小可用的更新源桥接功能,确保基础稳定性。
|
||||||
|
|
||||||
|
### 3.2 范围
|
||||||
|
|
||||||
|
| 包含 | 不包含(移至后续版本)|
|
||||||
|
|------|----------------------|
|
||||||
|
| JSON API 桥接 | Git 仓库支持 |
|
||||||
|
| 预置源(文派开源、ArkPress、AspireCloud)| WP-CLI |
|
||||||
|
| 基础缓存和降级 | 诊断工具 |
|
||||||
|
| 简单管理界面 | 配置导入导出 |
|
||||||
|
| 安全基础 | FAIR 支持 |
|
||||||
|
|
||||||
|
### 3.3 任务分解
|
||||||
|
|
||||||
|
#### 阶段 1:插件骨架(Day 1-2)
|
||||||
|
|
||||||
|
```
|
||||||
|
任务 1.1: 创建插件主文件
|
||||||
|
- wpbridge.php(插件头信息、激活/停用钩子)
|
||||||
|
- 预计:2 小时
|
||||||
|
|
||||||
|
任务 1.2: 自动加载器
|
||||||
|
- includes/Core/Loader.php
|
||||||
|
- PSR-4 风格自动加载
|
||||||
|
- 预计:1 小时
|
||||||
|
|
||||||
|
任务 1.3: 插件主类
|
||||||
|
- includes/Core/Plugin.php
|
||||||
|
- 单例模式,初始化各模块
|
||||||
|
- 预计:2 小时
|
||||||
|
|
||||||
|
任务 1.4: 设置管理
|
||||||
|
- includes/Core/Settings.php
|
||||||
|
- wp_options 读写封装
|
||||||
|
- 预计:2 小时
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 阶段 2:数据模型(Day 3-4)
|
||||||
|
|
||||||
|
```
|
||||||
|
任务 2.1: 源类型枚举
|
||||||
|
- includes/UpdateSource/SourceType.php
|
||||||
|
- 统一定义所有源类型
|
||||||
|
- 预计:1 小时
|
||||||
|
|
||||||
|
任务 2.2: 更新源模型
|
||||||
|
- includes/UpdateSource/SourceModel.php
|
||||||
|
- 数据结构、验证、序列化
|
||||||
|
- 预计:2 小时
|
||||||
|
|
||||||
|
任务 2.3: 源管理器
|
||||||
|
- includes/UpdateSource/SourceManager.php
|
||||||
|
- CRUD 操作、预置源加载
|
||||||
|
- 预计:3 小时
|
||||||
|
|
||||||
|
任务 2.4: 预置源配置
|
||||||
|
- includes/UpdateSource/PresetSources.php
|
||||||
|
- 文派开源、ArkPress、AspireCloud
|
||||||
|
- 预计:2 小时
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 阶段 3:核心桥接(Day 5-8)
|
||||||
|
|
||||||
|
```
|
||||||
|
任务 3.1: 处理器接口
|
||||||
|
- includes/UpdateSource/Handlers/HandlerInterface.php
|
||||||
|
- 统一接口定义
|
||||||
|
- 预计:1 小时
|
||||||
|
|
||||||
|
任务 3.2: JSON 处理器
|
||||||
|
- includes/UpdateSource/Handlers/JsonHandler.php
|
||||||
|
- Plugin Update Checker 格式兼容
|
||||||
|
- 预计:3 小时
|
||||||
|
|
||||||
|
任务 3.3: ArkPress 处理器
|
||||||
|
- includes/UpdateSource/Handlers/ArkPressHandler.php
|
||||||
|
- AspireCloud API 兼容
|
||||||
|
- 预计:3 小时
|
||||||
|
|
||||||
|
任务 3.4: AspireCloud 处理器
|
||||||
|
- includes/UpdateSource/Handlers/AspireCloudHandler.php
|
||||||
|
- 预计:2 小时
|
||||||
|
|
||||||
|
任务 3.5: 插件更新器
|
||||||
|
- includes/UpdateSource/PluginUpdater.php
|
||||||
|
- pre_set_site_transient_update_plugins 钩子
|
||||||
|
- plugins_api 钩子
|
||||||
|
- 预计:4 小时
|
||||||
|
|
||||||
|
任务 3.6: 主题更新器
|
||||||
|
- includes/UpdateSource/ThemeUpdater.php
|
||||||
|
- pre_set_site_transient_update_themes 钩子
|
||||||
|
- themes_api 钩子
|
||||||
|
- 预计:3 小时
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 阶段 4:缓存与降级(Day 9-10)
|
||||||
|
|
||||||
|
```
|
||||||
|
任务 4.1: 缓存管理器
|
||||||
|
- includes/Cache/CacheManager.php
|
||||||
|
- Transient 缓存封装
|
||||||
|
- 预计:2 小时
|
||||||
|
|
||||||
|
任务 4.2: 源健康检查
|
||||||
|
- includes/Cache/HealthChecker.php
|
||||||
|
- 连通性测试、状态缓存
|
||||||
|
- 预计:2 小时
|
||||||
|
|
||||||
|
任务 4.3: 降级策略
|
||||||
|
- includes/Cache/FallbackStrategy.php
|
||||||
|
- 过期缓存兜底、失败冷却
|
||||||
|
- 预计:2 小时
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 阶段 5:安全与日志(Day 11-12)
|
||||||
|
|
||||||
|
```
|
||||||
|
任务 5.1: 输入校验
|
||||||
|
- includes/Security/Validator.php
|
||||||
|
- URL 格式、版本号、JSON 结构
|
||||||
|
- 预计:2 小时
|
||||||
|
|
||||||
|
任务 5.2: 密钥加密
|
||||||
|
- includes/Security/Encryption.php
|
||||||
|
- API Key 加密存储
|
||||||
|
- 预计:2 小时
|
||||||
|
|
||||||
|
任务 5.3: 日志系统
|
||||||
|
- includes/Core/Logger.php
|
||||||
|
- 调试日志、错误日志
|
||||||
|
- 预计:2 小时
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 阶段 6:管理界面(Day 13-15)
|
||||||
|
|
||||||
|
```
|
||||||
|
任务 6.1: 管理页面
|
||||||
|
- includes/Admin/AdminPage.php
|
||||||
|
- 设置页面注册
|
||||||
|
- 预计:2 小时
|
||||||
|
|
||||||
|
任务 6.2: 源列表界面
|
||||||
|
- templates/admin/source-list.php
|
||||||
|
- WP_List_Table 实现
|
||||||
|
- 预计:3 小时
|
||||||
|
|
||||||
|
任务 6.3: 源编辑表单
|
||||||
|
- templates/admin/source-editor.php
|
||||||
|
- 添加/编辑更新源
|
||||||
|
- 预计:3 小时
|
||||||
|
|
||||||
|
任务 6.4: 样式和脚本
|
||||||
|
- assets/css/admin.css
|
||||||
|
- assets/js/admin.js
|
||||||
|
- 预计:2 小时
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
wpbridge/
|
||||||
|
├── wpbridge.php # 主文件
|
||||||
|
├── includes/
|
||||||
|
│ ├── Core/
|
||||||
|
│ │ ├── Plugin.php # 插件主类
|
||||||
|
│ │ ├── Loader.php # 自动加载
|
||||||
|
│ │ ├── Settings.php # 设置管理
|
||||||
|
│ │ └── Logger.php # 日志系统
|
||||||
|
│ │
|
||||||
|
│ ├── UpdateSource/
|
||||||
|
│ │ ├── SourceType.php # 源类型枚举
|
||||||
|
│ │ ├── SourceModel.php # 数据模型
|
||||||
|
│ │ ├── SourceManager.php # 源管理器
|
||||||
|
│ │ ├── PresetSources.php # 预置源配置
|
||||||
|
│ │ ├── PluginUpdater.php # 插件更新器
|
||||||
|
│ │ ├── ThemeUpdater.php # 主题更新器
|
||||||
|
│ │ └── Handlers/
|
||||||
|
│ │ ├── HandlerInterface.php
|
||||||
|
│ │ ├── JsonHandler.php
|
||||||
|
│ │ ├── ArkPressHandler.php
|
||||||
|
│ │ └── AspireCloudHandler.php
|
||||||
|
│ │
|
||||||
|
│ ├── Cache/
|
||||||
|
│ │ ├── CacheManager.php # 缓存管理
|
||||||
|
│ │ ├── HealthChecker.php # 健康检查
|
||||||
|
│ │ └── FallbackStrategy.php # 降级策略
|
||||||
|
│ │
|
||||||
|
│ ├── Security/
|
||||||
|
│ │ ├── Validator.php # 输入校验
|
||||||
|
│ │ └── Encryption.php # 密钥加密
|
||||||
|
│ │
|
||||||
|
│ └── Admin/
|
||||||
|
│ └── AdminPage.php # 管理页面
|
||||||
|
│
|
||||||
|
├── templates/
|
||||||
|
│ └── admin/
|
||||||
|
│ ├── source-list.php
|
||||||
|
│ └── source-editor.php
|
||||||
|
│
|
||||||
|
└── assets/
|
||||||
|
├── css/
|
||||||
|
│ └── admin.css
|
||||||
|
└── js/
|
||||||
|
└── admin.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 验收标准
|
||||||
|
|
||||||
|
1. **功能验收**
|
||||||
|
- [ ] 可添加自定义 JSON 更新源
|
||||||
|
- [ ] 预置源(文派开源)可正常检查更新
|
||||||
|
- [ ] 更新信息正确显示在 WordPress 后台
|
||||||
|
- [ ] 可下载并安装更新
|
||||||
|
|
||||||
|
2. **稳定性验收**
|
||||||
|
- [ ] 源不可用时不阻塞后台
|
||||||
|
- [ ] 缓存正常工作
|
||||||
|
- [ ] 错误信息用户友好
|
||||||
|
|
||||||
|
3. **安全验收**
|
||||||
|
- [ ] URL 格式校验有效
|
||||||
|
- [ ] API Key 加密存储
|
||||||
|
- [ ] 无 XSS/SQL 注入风险
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、v0.2.0 性能优化 + Git 支持
|
||||||
|
|
||||||
|
### 4.1 目标
|
||||||
|
|
||||||
|
实现性能优化核心功能,支持 Git 仓库作为更新源。
|
||||||
|
|
||||||
|
### 4.2 关键任务
|
||||||
|
|
||||||
|
#### 性能优化
|
||||||
|
- [ ] 并行请求管理器(ParallelRequestManager)
|
||||||
|
- [ ] 请求去重器(RequestDeduplicator)
|
||||||
|
- [ ] 条件请求(ConditionalRequest)
|
||||||
|
- [ ] 缓存分层(对象缓存 + DB)
|
||||||
|
- [ ] WP-Cron 后台预热(BackgroundUpdater)
|
||||||
|
|
||||||
|
#### Git 仓库支持
|
||||||
|
- [ ] GitHub 处理器(GitHubHandler)
|
||||||
|
- [ ] GitLab 处理器(GitLabHandler)
|
||||||
|
- [ ] Gitee 处理器(GiteeHandler)
|
||||||
|
- [ ] 菲码源库处理器(WenPaiGitHandler)
|
||||||
|
- [ ] 私有仓库认证
|
||||||
|
|
||||||
|
#### WP-CLI
|
||||||
|
- [ ] `wp bridge source` 命令组
|
||||||
|
- [ ] `wp bridge check` 命令
|
||||||
|
- [ ] `wp bridge cache` 命令
|
||||||
|
- [ ] `wp bridge diagnose` 命令
|
||||||
|
- [ ] `wp bridge config` 命令
|
||||||
|
|
||||||
|
#### 诊断工具
|
||||||
|
- [ ] 诊断页面
|
||||||
|
- [ ] 源连通性测试
|
||||||
|
- [ ] 诊断报告导出
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、v0.3.0 AI 桥接 + 商业插件
|
||||||
|
|
||||||
|
### 5.1 目标
|
||||||
|
|
||||||
|
实现 AI 服务桥接和商业插件支持。
|
||||||
|
|
||||||
|
### 5.2 关键任务
|
||||||
|
|
||||||
|
#### AI 桥接
|
||||||
|
- [ ] AI 设置数据模型(AISettings)
|
||||||
|
- [ ] AI 桥接主类(AIBridge)
|
||||||
|
- [ ] OpenAI 代理(OpenAIProxy)
|
||||||
|
- [ ] WPMind 转发器(WPMindForwarder)
|
||||||
|
- [ ] 白名单管理界面
|
||||||
|
|
||||||
|
#### 商业插件
|
||||||
|
- [ ] 商业插件检测
|
||||||
|
- [ ] 更新源覆盖
|
||||||
|
- [ ] 版本锁定
|
||||||
|
- [ ] 回滚机制
|
||||||
|
|
||||||
|
#### 源分组
|
||||||
|
- [ ] 源组数据模型
|
||||||
|
- [ ] 批量管理界面
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、v0.4.0 Cloud API
|
||||||
|
|
||||||
|
### 6.1 目标
|
||||||
|
|
||||||
|
提供云端 API 服务。
|
||||||
|
|
||||||
|
### 6.2 关键任务
|
||||||
|
|
||||||
|
- [ ] REST API 端点
|
||||||
|
- [ ] 认证机制
|
||||||
|
- [ ] 限流策略
|
||||||
|
- [ ] 文派叶子集成示例
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、v1.0.0 正式发布
|
||||||
|
|
||||||
|
### 7.1 目标
|
||||||
|
|
||||||
|
稳定版本,完善文档和用户体验。
|
||||||
|
|
||||||
|
### 7.2 关键任务
|
||||||
|
|
||||||
|
- [ ] 用户指南
|
||||||
|
- [ ] 开发者文档
|
||||||
|
- [ ] API 文档
|
||||||
|
- [ ] 设置向导
|
||||||
|
- [ ] 状态仪表板
|
||||||
|
- [ ] GitHub Release
|
||||||
|
- [ ] 菲码源库 Release
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、技术规范
|
||||||
|
|
||||||
|
### 8.1 编码规范
|
||||||
|
|
||||||
|
- PHP 7.4+ 兼容
|
||||||
|
- WordPress 编码标准
|
||||||
|
- PSR-4 自动加载
|
||||||
|
- 类型声明(PHP 7.4 风格)
|
||||||
|
|
||||||
|
### 8.2 测试要求
|
||||||
|
|
||||||
|
- 单元测试覆盖核心逻辑
|
||||||
|
- 集成测试覆盖 WordPress 钩子
|
||||||
|
- 手动测试覆盖 UI 交互
|
||||||
|
|
||||||
|
### 8.3 安全要求
|
||||||
|
|
||||||
|
- 所有用户输入必须校验
|
||||||
|
- 敏感数据加密存储
|
||||||
|
- 遵循 WordPress 安全最佳实践
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、风险与缓解
|
||||||
|
|
||||||
|
| 风险 | 影响 | 缓解措施 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 第三方 API 变更 | 处理器失效 | 模块化设计,易于更新 |
|
||||||
|
| 性能问题 | 后台卡顿 | 缓存优先,异步处理 |
|
||||||
|
| 安全漏洞 | 数据泄露 | 代码审计,安全测试 |
|
||||||
|
| 兼容性问题 | 插件冲突 | 最小化钩子使用,命名空间隔离 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、里程碑
|
||||||
|
|
||||||
|
| 里程碑 | 目标日期 | 交付物 |
|
||||||
|
|--------|----------|--------|
|
||||||
|
| M1: MVP 完成 | +3 周 | v0.1.0 可用版本 |
|
||||||
|
| M2: 性能优化 | +6 周 | v0.2.0 性能版本 |
|
||||||
|
| M3: AI 桥接 | +9 周 | v0.3.0 完整版本 |
|
||||||
|
| M4: 正式发布 | +12 周 | v1.0.0 稳定版本 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*最后更新: 2026-02-04*
|
||||||
463
DISCUSSION.md
Normal file
463
DISCUSSION.md
Normal file
|
|
@ -0,0 +1,463 @@
|
||||||
|
# WPBridge 讨论记录
|
||||||
|
|
||||||
|
> 产品设计和技术讨论的记录
|
||||||
|
|
||||||
|
*创建日期: 2026-02-04*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-02-04 - 最终定位确定
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
|
||||||
|
经过讨论,明确了 WPBridge 在文派生态中的定位:
|
||||||
|
|
||||||
|
- **文派叶子 (WPCY)** - 官方源加速(WordPress.org → 文派镜像),面向普通用户
|
||||||
|
- **WPBridge (文派云桥)** - 自定义源桥接,面向开发者/高级用户
|
||||||
|
|
||||||
|
### 最终定位
|
||||||
|
|
||||||
|
> **自定义源桥接器 - 让用户完全控制 WordPress 的外部连接**
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
|
||||||
|
```
|
||||||
|
WPBridge (文派云桥)
|
||||||
|
│
|
||||||
|
├── 📦 更新源桥接
|
||||||
|
│ ├── 第三方自托管插件/主题更新服务器
|
||||||
|
│ ├── 商业插件自定义更新源
|
||||||
|
│ ├── 私有仓库支持 (GitHub/GitLab)
|
||||||
|
│ └── 更新源管理界面
|
||||||
|
│
|
||||||
|
├── 🤖 AI 桥接
|
||||||
|
│ ├── OpenAI API 兼容层
|
||||||
|
│ ├── 商业插件 AI 适配器
|
||||||
|
│ └── 可选依赖 WPMind
|
||||||
|
│
|
||||||
|
└── 🔧 高级配置
|
||||||
|
├── 自定义 HTTP 头
|
||||||
|
├── 认证方式配置
|
||||||
|
└── 代理设置
|
||||||
|
```
|
||||||
|
|
||||||
|
### 与文派叶子的分工
|
||||||
|
|
||||||
|
| 功能 | 文派叶子 (WPCY) | WPBridge |
|
||||||
|
|------|-----------------|----------|
|
||||||
|
| WordPress.org 加速 | ✅ 主要功能 | ❌ 不做 |
|
||||||
|
| 自托管更新服务器 | ❌ 不做 | ✅ 主要功能 |
|
||||||
|
| 商业插件更新源 | ❌ 不做 | ✅ 主要功能 |
|
||||||
|
| AI 服务桥接 | ❌ 不做 | ✅ 主要功能 |
|
||||||
|
| 目标用户 | 普通用户 | 开发者/高级用户 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-02-04 - 愿景演进记录
|
||||||
|
|
||||||
|
### 初始愿景:AI Gateway
|
||||||
|
|
||||||
|
最初从 WPMind 的 AI Gateway 功能讨论中独立出来,定位为:
|
||||||
|
- OpenAI API 兼容层
|
||||||
|
- 商业插件 AI 适配器
|
||||||
|
|
||||||
|
### 扩展愿景:WordPress 云桥
|
||||||
|
|
||||||
|
讨论中发现更广阔的需求:
|
||||||
|
- 插件更新桥接
|
||||||
|
- 资源访问桥接
|
||||||
|
- 授权验证桥接
|
||||||
|
|
||||||
|
### 最终愿景:自定义源桥接器
|
||||||
|
|
||||||
|
明确与文派叶子的分工后,聚焦于:
|
||||||
|
- 自托管更新服务器
|
||||||
|
- 商业插件更新源
|
||||||
|
- AI 服务桥接
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文派生态全景
|
||||||
|
|
||||||
|
```
|
||||||
|
文派生态 (WenPai.org)
|
||||||
|
│
|
||||||
|
├── 📦 WPMirror (wpmirror.com)
|
||||||
|
│ └── 镜像源基础设施
|
||||||
|
│
|
||||||
|
├── 🇨🇳 LitePress (litepress.cn)
|
||||||
|
│ └── WordPress 中国定制版
|
||||||
|
│
|
||||||
|
├── 🍃 文派叶子 WPCY (wpcy.com)
|
||||||
|
│ ├── 中国源加速
|
||||||
|
│ ├── 插件/主题更新加速
|
||||||
|
│ ├── 翻译下载优化
|
||||||
|
│ └── 面向普通用户
|
||||||
|
│
|
||||||
|
├── 🤖 WPMind 文派心思
|
||||||
|
│ └── 纯 AI 应用(国内 AI 服务)
|
||||||
|
│
|
||||||
|
└── 🌉 WPBridge 文派云桥
|
||||||
|
├── 自定义更新源桥接
|
||||||
|
├── 商业插件更新源
|
||||||
|
├── AI 服务桥接
|
||||||
|
└── 面向开发者/高级用户
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 待深入讨论
|
||||||
|
|
||||||
|
### 1. 更新源桥接技术细节
|
||||||
|
|
||||||
|
**问题**:如何实现自托管更新服务器的桥接?
|
||||||
|
|
||||||
|
**技术方案**:
|
||||||
|
- 使用 `pre_set_site_transient_update_plugins` 钩子
|
||||||
|
- 使用 `pre_set_site_transient_update_themes` 钩子
|
||||||
|
- 自定义更新检查逻辑
|
||||||
|
|
||||||
|
**待讨论**:
|
||||||
|
- 更新源 API 格式标准?
|
||||||
|
- 是否兼容现有的更新服务器方案?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 商业插件更新源
|
||||||
|
|
||||||
|
**问题**:如何处理商业插件的更新源覆盖?
|
||||||
|
|
||||||
|
**挑战**:
|
||||||
|
- 不同商业插件有不同的更新机制
|
||||||
|
- 部分插件有授权验证
|
||||||
|
- 需要保持兼容性
|
||||||
|
|
||||||
|
**待讨论**:
|
||||||
|
- 是否提供预置的商业插件配置?
|
||||||
|
- 如何处理授权验证?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. AI 桥接与 WPMind 的关系
|
||||||
|
|
||||||
|
**问题**:AI 桥接功能是否依赖 WPMind?
|
||||||
|
|
||||||
|
**方案**:
|
||||||
|
- 无 WPMind:仅支持 OpenAI 兼容层(用户自己配置 API Key)
|
||||||
|
- 有 WPMind:支持国内 AI 服务商(使用 WPMind 的 Provider)
|
||||||
|
|
||||||
|
**待讨论**:
|
||||||
|
- 这种可选依赖的设计是否合理?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 与文派叶子的集成
|
||||||
|
|
||||||
|
**问题**:WPBridge 是否需要与文派叶子集成?
|
||||||
|
|
||||||
|
**选项**:
|
||||||
|
- A) 完全独立,互不干扰
|
||||||
|
- B) 检测文派叶子,提供互补功能
|
||||||
|
- C) 作为文派叶子的"高级扩展"
|
||||||
|
|
||||||
|
**待讨论**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 分发和定价
|
||||||
|
|
||||||
|
**问题**:WPBridge 如何分发和定价?
|
||||||
|
|
||||||
|
**分发选项**:
|
||||||
|
- GitHub Releases
|
||||||
|
- 文派官网
|
||||||
|
- WordPress.org(如果符合规范)
|
||||||
|
|
||||||
|
**定价选项**:
|
||||||
|
- 完全免费开源
|
||||||
|
- 核心免费 + 高级功能付费
|
||||||
|
- 订阅制
|
||||||
|
|
||||||
|
**待讨论**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-02-04 - 预置源与自托管方案讨论
|
||||||
|
|
||||||
|
### 背景
|
||||||
|
|
||||||
|
讨论了 WPBridge 需要支持的预置更新源和自托管方案。
|
||||||
|
|
||||||
|
### 关键决策
|
||||||
|
|
||||||
|
#### 1. 预置更新源
|
||||||
|
|
||||||
|
| 源 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **文派开源更新源** | 预置(默认) | 文派生态的开源插件/主题更新源 |
|
||||||
|
| **AspireCloud** | 可选预置 | AspirePress 的 CDN/API |
|
||||||
|
| **FAIR** | 可选预置 | Linux Foundation 的去中心化方案 |
|
||||||
|
|
||||||
|
#### 2. 自托管方案支持
|
||||||
|
|
||||||
|
| 方案 | 说明 | 兼容方式 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **ArkPress** | 文派开源的自托管组件,AspireCloud 分叉版本,针对中国用户优化 | 原生支持 |
|
||||||
|
| **AspireCloud** | AspirePress 的开源镜像/CDN | API 兼容 |
|
||||||
|
| **UpdatePulse Server** | 支持授权管理的自托管服务器 | JSON API 兼容 |
|
||||||
|
| **WP Packages Update Server** | GitHub 上的开源方案 | JSON API 兼容 |
|
||||||
|
| **Plugin Update Checker 格式** | 事实上的行业标准 | 原生支持 |
|
||||||
|
|
||||||
|
#### 3. WP-CLI 命令规范
|
||||||
|
|
||||||
|
确定使用 `wp bridge` 作为命令前缀(而非 `wp wpbridge`)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 源管理
|
||||||
|
wp bridge source list # 列出所有源
|
||||||
|
wp bridge source add <url> [--type=json] # 添加源
|
||||||
|
wp bridge source remove <id> # 删除源
|
||||||
|
wp bridge source enable <id> # 启用源
|
||||||
|
wp bridge source disable <id> # 禁用源
|
||||||
|
|
||||||
|
# 更新检查
|
||||||
|
wp bridge check # 检查所有源
|
||||||
|
wp bridge check <slug> # 检查指定插件/主题
|
||||||
|
|
||||||
|
# 缓存管理
|
||||||
|
wp bridge cache clear # 清除所有缓存
|
||||||
|
wp bridge cache status # 查看缓存状态
|
||||||
|
|
||||||
|
# 诊断
|
||||||
|
wp bridge diagnose # 生成诊断报告
|
||||||
|
wp bridge test <source_id> # 测试源连通性
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
wp bridge config export # 导出配置
|
||||||
|
wp bridge config import <file> # 导入配置
|
||||||
|
```
|
||||||
|
|
||||||
|
### 待讨论事项
|
||||||
|
|
||||||
|
#### 1. 源优先级和冲突处理
|
||||||
|
|
||||||
|
**问题**:如果同一个插件在多个源都有更新,如何处理?
|
||||||
|
|
||||||
|
**决策**:用户可设置优先级,默认选择版本号最高的
|
||||||
|
|
||||||
|
#### 2. 源分组和批量管理
|
||||||
|
|
||||||
|
**场景**:用户有多个来自同一厂商的商业插件
|
||||||
|
|
||||||
|
**决策**:引入"源组"概念,支持批量管理(v0.3.0)
|
||||||
|
|
||||||
|
#### 3. 更新通知策略
|
||||||
|
|
||||||
|
**决策**:与 WordPress 原生更新通知合并,邮件/Webhook 作为高级功能
|
||||||
|
|
||||||
|
#### 4. 版本锁定
|
||||||
|
|
||||||
|
**决策**:v0.2.0 实现基础版本锁定功能
|
||||||
|
|
||||||
|
#### 5. 安全扫描集成
|
||||||
|
|
||||||
|
**决策**:本地哈希校验为基础,VirusTotal/Patchstack 作为付费功能
|
||||||
|
|
||||||
|
#### 6. 统计和分析
|
||||||
|
|
||||||
|
**决策**:默认不收集,匿名统计作为可选(需用户同意)
|
||||||
|
|
||||||
|
#### 7. 白标/OEM 支持
|
||||||
|
|
||||||
|
**决策**:作为付费功能,v1.0.0 后考虑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-02-04 - Codex 深度讨论(第二轮)
|
||||||
|
|
||||||
|
### 讨论主题
|
||||||
|
|
||||||
|
基于市场研究报告,与 Codex 进行了 5 个核心问题的深度讨论。
|
||||||
|
|
||||||
|
### 关键结论
|
||||||
|
|
||||||
|
#### 1. 性能优化作为核心卖点
|
||||||
|
|
||||||
|
**结论:应当成为核心价值主张之一**
|
||||||
|
|
||||||
|
**"双锚点"价值主张:**
|
||||||
|
- 对普通痛点:后台加载快
|
||||||
|
- 对目标用户:可控的自定义源桥接
|
||||||
|
|
||||||
|
**7 项性能优化措施:**
|
||||||
|
1. 请求合并与并行(`Requests::request_multiple`)
|
||||||
|
2. 智能缓存(结果缓存 + 健康状态缓存)
|
||||||
|
3. 请求去重(短时间合并窗口 + 锁)
|
||||||
|
4. 分组检查(同一厂商多插件共享一次请求)
|
||||||
|
5. 条件请求(`If-Modified-Since` / `ETag`)
|
||||||
|
6. 缓存分层(Redis/Memcached 优先,DB 兜底)
|
||||||
|
7. 失败兜底(返回旧版本信息,不阻塞后台)
|
||||||
|
|
||||||
|
#### 2. 与 FAIR 的关系
|
||||||
|
|
||||||
|
- WPBridge 把 FAIR 作为**可选更新源**之一
|
||||||
|
- 默认不替换 WordPress.org
|
||||||
|
- 产品表述:**"WPBridge 不替换官方源,只做用户自定义桥接"**
|
||||||
|
|
||||||
|
#### 3. 商业插件法律边界
|
||||||
|
|
||||||
|
**稳妥策略:**
|
||||||
|
- 以"配置框架"优先,不预置具体厂商地址
|
||||||
|
- 仅在得到授权时提供预置
|
||||||
|
- 坚持"用户提供授权信息"
|
||||||
|
- 避免"绕过授权"功能
|
||||||
|
- 本地缓存仅限站点内
|
||||||
|
|
||||||
|
**对外表述:**
|
||||||
|
> "WPBridge 不破解、不绕过授权,仅提供合规的配置与转发能力。"
|
||||||
|
|
||||||
|
#### 4. 中国市场特殊性
|
||||||
|
|
||||||
|
**与 WPCY 协同:**
|
||||||
|
- WPBridge 检测 WPCY 存在时,官方源自动走 WPCY
|
||||||
|
- 避免重复功能
|
||||||
|
|
||||||
|
**中国特色功能:**
|
||||||
|
- 源级别的"优选地域节点/备用源"
|
||||||
|
- 国内 Git 供应商支持(Gitee)
|
||||||
|
- 更短超时 + 智能重试策略
|
||||||
|
|
||||||
|
#### 5. 技术架构建议
|
||||||
|
|
||||||
|
**WP-Cron 后台任务:**
|
||||||
|
- 引入 `wpbridge_update_sources` 定时任务
|
||||||
|
- 前台仅读取缓存,避免后台页面发起大量请求
|
||||||
|
|
||||||
|
**降级策略:**
|
||||||
|
1. 优先使用"上次成功缓存"
|
||||||
|
2. 标记源为 `degraded/failed`
|
||||||
|
3. 自动切换备用源
|
||||||
|
4. 管理界面提示,不阻塞后台
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-02-04 - Codex 评审反馈
|
||||||
|
|
||||||
|
### 评审概要
|
||||||
|
|
||||||
|
使用 OpenAI Codex (gpt-5.2-codex-xhigh) 对项目文档进行了全面评审。
|
||||||
|
|
||||||
|
### 主要反馈
|
||||||
|
|
||||||
|
#### 1. 项目定位与边界
|
||||||
|
|
||||||
|
**认可:**
|
||||||
|
- 定位整体清晰,"自定义源桥接"与"官方源加速"形成互补
|
||||||
|
- 目标用户画像准确
|
||||||
|
|
||||||
|
**建议:**
|
||||||
|
- 边界需要再硬化,明确"非目标"
|
||||||
|
- 云 API 定义为"桥接能力的远程形态",不承担镜像/加速职责
|
||||||
|
- 补充"使用动机"优先级排序
|
||||||
|
|
||||||
|
#### 2. 技术架构
|
||||||
|
|
||||||
|
**建议改进:**
|
||||||
|
- 更新源适配器做成**统一接口+能力矩阵**
|
||||||
|
- **缓存与降级策略**要提前设计
|
||||||
|
- **安全边界**:URL 校验、密钥加密存储、下载包校验
|
||||||
|
- AI 拦截做成**显式白名单**
|
||||||
|
- 云 API 需明确认证、限流与可用性 SLA
|
||||||
|
|
||||||
|
#### 3. 路线图优先级
|
||||||
|
|
||||||
|
**建议调整:**
|
||||||
|
- 把"稳定性/可运维能力"前置
|
||||||
|
- 先做"通用自定义源"跑通,再添加预置模板
|
||||||
|
- AI 桥接可与核心解耦
|
||||||
|
|
||||||
|
#### 4. AI 桥接与 WPMind 依赖
|
||||||
|
|
||||||
|
**建议:**
|
||||||
|
- 明确"无 WPMind 时"的稳定行为
|
||||||
|
- 定义稳定接口层,减少直接耦合
|
||||||
|
|
||||||
|
#### 5. 定价策略
|
||||||
|
|
||||||
|
**建议:**
|
||||||
|
- 考虑"基础终身 + 更新/支持年费"混合模式
|
||||||
|
- 付费点围绕企业刚需
|
||||||
|
|
||||||
|
### 采纳决策
|
||||||
|
|
||||||
|
| 建议 | 决策 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 明确"非目标" | ✅ 采纳 | 添加到 CLAUDE.md |
|
||||||
|
| 统一接口+能力矩阵 | ✅ 采纳 | 更新架构设计 |
|
||||||
|
| 缓存与降级策略 | ✅ 采纳 | 加入 v0.1.0 |
|
||||||
|
| 安全边界设计 | ✅ 采纳 | 加入 v0.1.0 |
|
||||||
|
| AI 拦截白名单 | ✅ 采纳 | 更新设计文档 |
|
||||||
|
| 稳定性前置 | ✅ 采纳 | 调整路线图 |
|
||||||
|
| 混合定价模式 | 🔄 待定 | 需进一步讨论 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-02-04 - Codex 评审问题修复
|
||||||
|
|
||||||
|
### 问题清单与修复状态
|
||||||
|
|
||||||
|
#### HIGH 严重性
|
||||||
|
|
||||||
|
| 问题 | 状态 | 修复说明 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 源类型不一致 | ✅ 已修复 | 创建统一的 `SourceType` 枚举,包含所有类型(json/github/gitlab/gitee/wenpai_git/zip/arkpress/aspirecloud/fair/puc),更新数据模型引用 |
|
||||||
|
| AI 设置配置与实现不匹配 | ✅ 已修复 | 创建 `AISettings` 数据模型,`mode` 和 `whitelist` 由用户配置驱动,不再硬编码 |
|
||||||
|
|
||||||
|
#### MEDIUM 严重性
|
||||||
|
|
||||||
|
| 问题 | 状态 | 修复说明 |
|
||||||
|
|------|------|----------|
|
||||||
|
| Cloud API 不在路线图 | ✅ 已修复 | 添加 v0.4.0 版本专门处理 Cloud API |
|
||||||
|
| v0.1.0 范围太大 | ✅ 已修复 | 精简为 MVP,移除 Git 支持、WP-CLI、诊断工具到 v0.2.0 |
|
||||||
|
| 性能优化未明确 | ✅ 已修复 | 性能优化明确放在 v0.2.0,作为该版本核心任务 |
|
||||||
|
|
||||||
|
### 修改的文件
|
||||||
|
|
||||||
|
1. **DESIGN.md**
|
||||||
|
- 添加 `SourceType` 枚举统一定义
|
||||||
|
- 添加类型与处理器映射表
|
||||||
|
- 更新数据模型引用枚举
|
||||||
|
- 重写 `AISettings` 和 `AIBridge` 类,配置驱动
|
||||||
|
|
||||||
|
2. **ROADMAP.md**
|
||||||
|
- 调整版本规划(v0.1.0 → v0.2.0 → v0.3.0 → v0.4.0 → v1.0.0)
|
||||||
|
- 精简 v0.1.0 范围为 MVP
|
||||||
|
- 性能优化移至 v0.2.0 核心任务
|
||||||
|
- 商业插件适配移至 v0.3.0
|
||||||
|
- 新增 v0.4.0 Cloud API
|
||||||
|
|
||||||
|
3. **DEVELOPMENT-PLAN.md**(新建)
|
||||||
|
- 完整的开发计划
|
||||||
|
- 详细的任务分解
|
||||||
|
- 文件结构设计
|
||||||
|
- 验收标准
|
||||||
|
- 风险与缓解
|
||||||
|
- 里程碑规划
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下一步行动
|
||||||
|
|
||||||
|
1. [x] 深入讨论更新源桥接的技术细节
|
||||||
|
2. [x] 确定与文派叶子的集成方式
|
||||||
|
3. [x] 确定 AI 桥接与 WPMind 的依赖关系
|
||||||
|
4. [x] 确定分发和定价策略
|
||||||
|
5. [x] 完成业务流程和架构设计
|
||||||
|
6. [x] 修复 Codex 评审问题
|
||||||
|
7. [x] 创建完整开发计划
|
||||||
|
8. [ ] Codex 最终确认
|
||||||
|
9. [ ] 开始 v0.1.0 开发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*最后更新: 2026-02-04*
|
||||||
382
RESEARCH.md
Normal file
382
RESEARCH.md
Normal file
|
|
@ -0,0 +1,382 @@
|
||||||
|
# WPBridge 市场研究报告
|
||||||
|
|
||||||
|
> WordPress 更新生态现状与 WPBridge 机会分析
|
||||||
|
|
||||||
|
*创建日期: 2026-02-04*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. WordPress 更新生态现状
|
||||||
|
|
||||||
|
### 1.1 核心痛点:后台卡死问题
|
||||||
|
|
||||||
|
WordPress 后台慢是一个普遍问题,主要原因之一是**大量插件各自发起更新检查请求**:
|
||||||
|
|
||||||
|
```
|
||||||
|
WordPress 后台加载时
|
||||||
|
│
|
||||||
|
├── 核心更新检查 → api.wordpress.org
|
||||||
|
├── 插件 A 更新检查 → plugin-a.com/api
|
||||||
|
├── 插件 B 更新检查 → plugin-b.com/api
|
||||||
|
├── 插件 C 授权验证 → license.plugin-c.com
|
||||||
|
├── 主题更新检查 → theme-vendor.com/api
|
||||||
|
├── ...(可能 10-50 个请求)
|
||||||
|
│
|
||||||
|
└── 结果:后台加载 5-30 秒
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题根源:**
|
||||||
|
- 每个商业插件都有自己的更新服务器
|
||||||
|
- 每个插件独立发起 HTTP 请求
|
||||||
|
- 没有统一的缓存和批量机制
|
||||||
|
- 授权验证增加额外延迟
|
||||||
|
- 中国用户访问国外服务器更慢
|
||||||
|
|
||||||
|
### 1.2 生态碎片化
|
||||||
|
|
||||||
|
| 更新源类型 | 示例 | 问题 |
|
||||||
|
|-----------|------|------|
|
||||||
|
| WordPress.org | 官方免费插件 | 中国访问慢 |
|
||||||
|
| 商业插件自建 | Elementor Pro, ACF Pro | 各自为政 |
|
||||||
|
| GitHub Releases | 开发者插件 | 需要手动配置 |
|
||||||
|
| EDD Software Licensing | 大量商业插件 | 每个都要授权检查 |
|
||||||
|
| WooCommerce.com | WooCommerce 扩展 | 独立的更新系统 |
|
||||||
|
| Envato Market | ThemeForest 主题 | 又一个独立系统 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 行业解决方案分析
|
||||||
|
|
||||||
|
### 2.1 FAIR Package Manager
|
||||||
|
|
||||||
|
**背景:** 2024年9月 WP Engine 事件后,Linux Foundation 发起的去中心化项目
|
||||||
|
|
||||||
|
**定位:** 联邦式独立仓库(Federated and Independent Repository)
|
||||||
|
|
||||||
|
**核心特点:**
|
||||||
|
- 去中心化的插件/主题分发
|
||||||
|
- 多镜像源支持
|
||||||
|
- 由 Linux Foundation 治理
|
||||||
|
- 300+ 贡献者参与
|
||||||
|
|
||||||
|
**技术实现:**
|
||||||
|
- WordPress 插件形式
|
||||||
|
- 替换默认的 WordPress.org 更新源
|
||||||
|
- 支持多个可信源
|
||||||
|
|
||||||
|
**挑战:**
|
||||||
|
- 需要大规模采用才能有效
|
||||||
|
- 托管商需要主动支持
|
||||||
|
- 安全性担忧(多个潜在攻击点)
|
||||||
|
- 不解决商业插件问题
|
||||||
|
|
||||||
|
**与 WPBridge 的区别:**
|
||||||
|
|
||||||
|
| 方面 | FAIR | WPBridge |
|
||||||
|
|------|------|----------|
|
||||||
|
| 定位 | 基础设施替代 | 用户侧桥接器 |
|
||||||
|
| 目标 | 替换 WordPress.org | 补充自定义源 |
|
||||||
|
| 用户 | 所有 WordPress 用户 | 开发者/高级用户 |
|
||||||
|
| 商业插件 | 不直接解决 | 核心功能 |
|
||||||
|
| 依赖 | 需要生态采用 | 用户自主配置 |
|
||||||
|
|
||||||
|
### 2.2 Plugin Update Checker
|
||||||
|
|
||||||
|
**作者:** YahnisElsts
|
||||||
|
|
||||||
|
**定位:** 开发者库,用于自定义更新服务器
|
||||||
|
|
||||||
|
**特点:**
|
||||||
|
- PHP 库,嵌入插件代码
|
||||||
|
- 支持 JSON API、GitHub、GitLab
|
||||||
|
- 被大量商业插件使用
|
||||||
|
- 事实上的行业标准
|
||||||
|
|
||||||
|
**局限:**
|
||||||
|
- 每个插件独立集成
|
||||||
|
- 用户无法统一管理
|
||||||
|
- 不解决性能问题
|
||||||
|
|
||||||
|
### 2.3 现有更新管理插件
|
||||||
|
|
||||||
|
| 插件 | 功能 | 局限 |
|
||||||
|
|------|------|------|
|
||||||
|
| Easy Updates Manager | 控制自动更新 | 不支持自定义源 |
|
||||||
|
| ManageWP | 多站点管理 | SaaS 服务,需付费 |
|
||||||
|
| MainWP | 自托管多站点 | 复杂,面向代理商 |
|
||||||
|
| InfiniteWP | 多站点管理 | 不解决更新源问题 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. WPBridge 机会分析
|
||||||
|
|
||||||
|
### 3.1 差异化定位
|
||||||
|
|
||||||
|
```
|
||||||
|
市场空白
|
||||||
|
│
|
||||||
|
├── FAIR → 基础设施层(替换 WordPress.org)
|
||||||
|
├── Plugin Update Checker → 开发者层(嵌入插件)
|
||||||
|
│
|
||||||
|
└── WPBridge → 用户层(统一管理自定义源)
|
||||||
|
├── 不替换 WordPress.org
|
||||||
|
├── 不需要修改插件代码
|
||||||
|
└── 用户自主配置和管理
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 核心价值主张
|
||||||
|
|
||||||
|
**对于企业用户:**
|
||||||
|
- 内网部署,私有仓库
|
||||||
|
- 供应链安全控制
|
||||||
|
- 合规性要求
|
||||||
|
|
||||||
|
**对于开发者:**
|
||||||
|
- 测试环境灵活配置
|
||||||
|
- 自托管插件分发
|
||||||
|
- 版本控制和回滚
|
||||||
|
|
||||||
|
**对于商业插件用户:**
|
||||||
|
- 统一管理多个更新源
|
||||||
|
- 减少授权验证延迟
|
||||||
|
- 备用更新渠道
|
||||||
|
|
||||||
|
### 3.3 性能优化机会
|
||||||
|
|
||||||
|
```
|
||||||
|
当前状态(无 WPBridge)
|
||||||
|
├── 插件 A → HTTP 请求 1
|
||||||
|
├── 插件 B → HTTP 请求 2
|
||||||
|
├── 插件 C → HTTP 请求 3
|
||||||
|
└── 总计:N 个串行请求
|
||||||
|
|
||||||
|
使用 WPBridge 后
|
||||||
|
├── WPBridge 统一检查
|
||||||
|
│ ├── 缓存命中 → 直接返回
|
||||||
|
│ ├── 批量请求 → 减少连接数
|
||||||
|
│ └── 并行处理 → 减少等待
|
||||||
|
└── 总计:显著减少请求时间
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 竞争格局
|
||||||
|
|
||||||
|
### 4.1 直接竞争
|
||||||
|
|
||||||
|
| 竞品 | 定位 | WPBridge 优势 |
|
||||||
|
|------|------|---------------|
|
||||||
|
| FAIR | 基础设施替代 | 更轻量,用户自主 |
|
||||||
|
| AspirePress | 类似 FAIR | 同上 |
|
||||||
|
|
||||||
|
### 4.2 间接竞争
|
||||||
|
|
||||||
|
| 竞品 | 定位 | WPBridge 优势 |
|
||||||
|
|------|------|---------------|
|
||||||
|
| ManageWP | SaaS 多站点 | 自托管,无订阅 |
|
||||||
|
| MainWP | 自托管多站点 | 更简单,专注更新 |
|
||||||
|
| 文派叶子 | 官方源加速 | 自定义源,互补 |
|
||||||
|
|
||||||
|
### 4.3 合作机会
|
||||||
|
|
||||||
|
- **文派叶子**:官方源加速 + WPBridge 自定义源 = 完整解决方案
|
||||||
|
- **FAIR**:可以作为 WPBridge 的一个更新源
|
||||||
|
- **Plugin Update Checker**:兼容其 JSON 格式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 建议
|
||||||
|
|
||||||
|
### 5.1 核心功能优先级
|
||||||
|
|
||||||
|
| 优先级 | 功能 | 理由 |
|
||||||
|
|--------|------|------|
|
||||||
|
| P0 | 自定义 JSON 更新源 | 最通用,兼容 PUC |
|
||||||
|
| P0 | 缓存和性能优化 | 解决核心痛点 |
|
||||||
|
| P1 | GitHub/GitLab 支持 | 开发者刚需 |
|
||||||
|
| P1 | 源健康检查 | 稳定性保障 |
|
||||||
|
| P2 | 商业插件预置 | 提升易用性 |
|
||||||
|
| P3 | AI 桥接 | 差异化功能 |
|
||||||
|
|
||||||
|
### 5.2 差异化策略
|
||||||
|
|
||||||
|
1. **性能优先**:强调减少后台加载时间
|
||||||
|
2. **用户自主**:不依赖外部服务,用户完全控制
|
||||||
|
3. **生态兼容**:与文派叶子、FAIR 互补而非竞争
|
||||||
|
4. **中国优化**:针对中国网络环境优化
|
||||||
|
|
||||||
|
### 5.3 市场定位
|
||||||
|
|
||||||
|
```
|
||||||
|
WPBridge 一句话定位:
|
||||||
|
|
||||||
|
"让 WordPress 后台不再卡死 —— 统一管理你的插件更新源"
|
||||||
|
|
||||||
|
或
|
||||||
|
|
||||||
|
"自定义源桥接器 —— 企业内网、商业插件、私有仓库,一个插件搞定"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 自托管更新服务器生态
|
||||||
|
|
||||||
|
### 6.1 主要方案对比
|
||||||
|
|
||||||
|
| 方案 | 类型 | 特点 | 适用场景 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| **ArkPress** | 自托管镜像 | AspireCloud 分叉,中国优化 | 中国企业/开发者 |
|
||||||
|
| **AspireCloud** | 开源镜像 | FAIR 基础设施,联邦模式 | 国际用户 |
|
||||||
|
| **UpdatePulse Server** | 自托管服务器 | 授权管理、VCS 集成 | 商业插件开发者 |
|
||||||
|
| **WP Packages Update Server** | 自托管服务器 | 轻量级 | 小型团队 |
|
||||||
|
| **Plugin Update Checker** | 开发者库 | 行业标准 | 插件开发者 |
|
||||||
|
|
||||||
|
### 6.2 ArkPress(文派开源)
|
||||||
|
|
||||||
|
**定位**:AspireCloud 的中国分叉版本,针对中国网络环境优化
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 国内服务器部署
|
||||||
|
- 中文界面
|
||||||
|
- 与文派生态深度集成
|
||||||
|
- 更适合中国用户的网络环境
|
||||||
|
|
||||||
|
**与 WPBridge 的关系**:
|
||||||
|
- ArkPress 是服务端(自托管更新服务器)
|
||||||
|
- WPBridge 是客户端(连接各种更新源的桥接器)
|
||||||
|
- 两者配合使用,提供完整的自托管更新解决方案
|
||||||
|
|
||||||
|
### 6.3 AspirePress 生态
|
||||||
|
|
||||||
|
**组件**:
|
||||||
|
- **AspireCloud**:CDN/API 服务,为 FAIR 提供基础设施
|
||||||
|
- **AspireUpdate**:WordPress 插件,连接 AspireCloud
|
||||||
|
- **AspireSync**:同步工具
|
||||||
|
- **AspireExplorer**:浏览器界面
|
||||||
|
|
||||||
|
**参考**:
|
||||||
|
- [AspirePress 官网](https://aspirepress.org/)
|
||||||
|
- [AspireCloud 文档](https://docs.aspirepress.org/aspirecloud/)
|
||||||
|
- [AspireUpdate GitHub](https://github.com/aspirepress/AspireUpdate/)
|
||||||
|
|
||||||
|
### 6.4 UpdatePulse Server
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 支持授权管理(License Key)
|
||||||
|
- 支持 VCS 集成(GitHub/GitLab/Bitbucket)
|
||||||
|
- 支持云存储(S3 兼容)
|
||||||
|
- 与 Plugin Update Checker 兼容
|
||||||
|
|
||||||
|
**参考**:
|
||||||
|
- [UpdatePulse Server - WordPress.org](https://wordpress.org/plugins/updatepulse-server/)
|
||||||
|
- [UpdatePulse Server - 文派](https://wenpai.org/plugins/updatepulse-server/)
|
||||||
|
|
||||||
|
### 6.5 WPBridge 兼容策略
|
||||||
|
|
||||||
|
```
|
||||||
|
WPBridge 兼容层
|
||||||
|
│
|
||||||
|
├── 原生支持
|
||||||
|
│ ├── 文派开源更新源
|
||||||
|
│ ├── ArkPress API
|
||||||
|
│ ├── Plugin Update Checker JSON 格式
|
||||||
|
│ └── AspireCloud API
|
||||||
|
│
|
||||||
|
├── 通用 JSON API 支持
|
||||||
|
│ ├── UpdatePulse Server
|
||||||
|
│ ├── WP Packages Update Server
|
||||||
|
│ └── 其他兼容 PUC 格式的服务器
|
||||||
|
│
|
||||||
|
└── Git 仓库支持
|
||||||
|
├── GitHub Releases
|
||||||
|
├── GitLab Releases
|
||||||
|
├── Gitee Releases(国内)
|
||||||
|
└── 菲码源库(文派)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 技术实现研究
|
||||||
|
|
||||||
|
- [FAIR Package Manager - WP Umbrella](https://wp-umbrella.com/blog/the-fair-package-manager/)
|
||||||
|
- [FAIR Package Manager - WPShout](https://wpshout.com/fair-package-manager-wordpress-org-alternative/)
|
||||||
|
- [Plugin Update Checker - GitHub](https://github.com/YahnisElsts/plugin-update-checker)
|
||||||
|
- [75 Slow WordPress Plugins](https://onlinemediamasters.com/slow-wordpress-plugins/)
|
||||||
|
- [Speed Up WordPress Backend - WPShout](https://wpshout.com/speed-up-wordpress-backend/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 技术实现研究
|
||||||
|
|
||||||
|
### 7.1 WordPress 并行请求 API
|
||||||
|
|
||||||
|
WordPress 内置 `Requests::request_multiple()` 方法,支持并行 HTTP 请求:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 并行请求示例
|
||||||
|
$requests = [
|
||||||
|
'source1' => ['url' => 'https://api1.example.com/update.json'],
|
||||||
|
'source2' => ['url' => 'https://api2.example.com/update.json'],
|
||||||
|
'source3' => ['url' => 'https://api3.example.com/update.json'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$responses = \WpOrg\Requests\Requests::request_multiple(
|
||||||
|
$requests,
|
||||||
|
['timeout' => 10]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 结果:3 个请求并行执行,总时间 ≈ 最慢的那个请求
|
||||||
|
// 而非串行执行的 3 倍时间
|
||||||
|
```
|
||||||
|
|
||||||
|
**参考:**
|
||||||
|
- [WordPress Trac #33055 - Support Parallel HTTP Requests](https://core.trac.wordpress.org/ticket/33055)
|
||||||
|
- [WordPress Trac #44118 - Unnecessary plugin update checks](https://core.trac.wordpress.org/ticket/44118)
|
||||||
|
|
||||||
|
### 7.2 更新检查钩子
|
||||||
|
|
||||||
|
WordPress 使用以下钩子处理更新检查:
|
||||||
|
|
||||||
|
| 钩子 | 用途 | WPBridge 使用方式 |
|
||||||
|
|------|------|-------------------|
|
||||||
|
| `pre_set_site_transient_update_plugins` | 插件更新检查前 | 注入自定义源的更新信息 |
|
||||||
|
| `pre_set_site_transient_update_themes` | 主题更新检查前 | 注入自定义源的更新信息 |
|
||||||
|
| `plugins_api` | 插件详情 API | 返回自定义源的插件信息 |
|
||||||
|
| `themes_api` | 主题详情 API | 返回自定义源的主题信息 |
|
||||||
|
| `upgrader_pre_download` | 下载前 | 替换下载 URL |
|
||||||
|
|
||||||
|
### 7.3 性能优化技术栈
|
||||||
|
|
||||||
|
| 技术 | 用途 | WordPress 支持 |
|
||||||
|
|------|------|----------------|
|
||||||
|
| `Requests::request_multiple` | 并行 HTTP 请求 | ✅ 内置 |
|
||||||
|
| `set_transient` / `get_transient` | 数据库缓存 | ✅ 内置 |
|
||||||
|
| `wp_cache_*` | 对象缓存 | ✅ 内置(需 Redis/Memcached) |
|
||||||
|
| `wp_schedule_event` | 后台定时任务 | ✅ 内置 |
|
||||||
|
| HTTP 条件请求 | ETag/Last-Modified | ✅ 需手动实现 |
|
||||||
|
|
||||||
|
### 7.4 与 WPCY 协同检测
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 检测文派叶子是否存在
|
||||||
|
function wpbridge_detect_wpcy(): bool {
|
||||||
|
return defined('STARTER_PLUGIN_VERSION') ||
|
||||||
|
class_exists('WP_China_Yes') ||
|
||||||
|
function_exists('wpcy_is_active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 WPCY 存在,官方源走 WPCY,自定义源走 WPBridge
|
||||||
|
function wpbridge_should_handle_source(string $url): bool {
|
||||||
|
if (wpbridge_detect_wpcy()) {
|
||||||
|
// 官方源让 WPCY 处理
|
||||||
|
if (strpos($url, 'api.wordpress.org') !== false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*最后更新: 2026-02-04*
|
||||||
404
ROADMAP.md
Normal file
404
ROADMAP.md
Normal file
|
|
@ -0,0 +1,404 @@
|
||||||
|
# WPBridge 开发路线图
|
||||||
|
|
||||||
|
> 自定义源桥接器 - 版本规划和开发任务
|
||||||
|
|
||||||
|
*创建日期: 2026-02-04*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前版本: v0.8.0
|
||||||
|
|
||||||
|
## 版本规划
|
||||||
|
|
||||||
|
```
|
||||||
|
v0.1.0 - MVP:最小可用桥接 + 基础缓存/降级 ✅ 已完成
|
||||||
|
↓
|
||||||
|
v0.2.0 - Git 仓库支持 + WP-CLI + 性能优化 ✅ 已完成
|
||||||
|
↓
|
||||||
|
v0.3.0 - 源分组 + 商业插件检测 ✅ 已完成
|
||||||
|
↓
|
||||||
|
v0.4.0 - Bridge API ✅ 已完成
|
||||||
|
↓
|
||||||
|
v0.8.0 - 配置导入导出 + 稳定性优化 ✅ 已完成
|
||||||
|
↓
|
||||||
|
v0.9.0 - 版本控制 + 用户体验 ← 当前目标
|
||||||
|
↓
|
||||||
|
v1.0.0 - 正式发布
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.1.0 - MVP:最小可用桥接 + 基础缓存/降级 ✅
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
实现最小可用的更新源桥接功能,确保基础稳定性
|
||||||
|
|
||||||
|
### 任务清单
|
||||||
|
|
||||||
|
#### 插件基础结构
|
||||||
|
- [x] 主文件 `wpbridge.php`
|
||||||
|
- [x] 自动加载器 `Loader.php`
|
||||||
|
- [x] 设置页面框架 `Settings.php`
|
||||||
|
- [x] 数据存储结构
|
||||||
|
|
||||||
|
#### 预置更新源
|
||||||
|
- [x] 文派开源更新源(默认启用)
|
||||||
|
- [x] ArkPress 支持(文派自托管方案)
|
||||||
|
- [x] AspireCloud 支持(可选)
|
||||||
|
|
||||||
|
#### 更新源管理(基础)
|
||||||
|
- [x] 更新源数据模型(使用统一 SourceType 枚举)
|
||||||
|
- [x] 更新源 CRUD 操作
|
||||||
|
- [x] 更新源列表界面
|
||||||
|
- [x] 添加/编辑更新源表单
|
||||||
|
|
||||||
|
#### 核心桥接功能
|
||||||
|
- [x] `pre_set_site_transient_update_plugins` 钩子
|
||||||
|
- [x] `pre_set_site_transient_update_themes` 钩子
|
||||||
|
- [x] JSON API 处理器(JsonHandler)
|
||||||
|
- [x] ArkPress 处理器(ArkPressHandler)
|
||||||
|
- [x] AspireCloud 处理器(AspireCloudHandler)
|
||||||
|
- [x] Plugin Update Checker JSON 格式兼容(PUCHandler)
|
||||||
|
|
||||||
|
#### 基础缓存与降级
|
||||||
|
- [x] Transient 缓存(12 小时 TTL)
|
||||||
|
- [x] 源健康状态缓存(1 小时 TTL)
|
||||||
|
- [x] 失败源冷却机制(30 分钟)
|
||||||
|
- [x] 过期缓存兜底(源不可用时返回旧数据)
|
||||||
|
- [x] 请求超时限制(10 秒)
|
||||||
|
|
||||||
|
#### 安全基础
|
||||||
|
- [x] URL 格式校验
|
||||||
|
- [x] API Key 加密存储
|
||||||
|
- [x] JSON 响应结构校验
|
||||||
|
|
||||||
|
#### 日志与错误处理
|
||||||
|
- [x] 调试日志(可开关)
|
||||||
|
- [x] 用户友好的错误信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.2.0 - Git 仓库支持 + WP-CLI + 性能优化 ✅
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
支持 Git 仓库作为更新源,提供命令行工具,实现性能优化
|
||||||
|
|
||||||
|
### 任务清单
|
||||||
|
|
||||||
|
#### 性能优化(核心)
|
||||||
|
- [x] 并行请求(`ParallelRequestManager`)
|
||||||
|
- [x] 条件请求(ETag/Last-Modified)
|
||||||
|
- [x] WP-Cron 后台预热任务(`BackgroundUpdater`)
|
||||||
|
- [ ] 请求去重与合并窗口
|
||||||
|
- [ ] 缓存分层(对象缓存优先,DB 兜底)
|
||||||
|
|
||||||
|
#### WP-CLI 支持(`wp bridge`)
|
||||||
|
- [x] `wp bridge source list` - 列出所有源
|
||||||
|
- [x] `wp bridge source add <url>` - 添加源
|
||||||
|
- [x] `wp bridge source remove <id>` - 删除源
|
||||||
|
- [x] `wp bridge source enable/disable <id>` - 启用/禁用源
|
||||||
|
- [x] `wp bridge check` - 检查所有源
|
||||||
|
- [x] `wp bridge cache clear` - 清除缓存
|
||||||
|
- [x] `wp bridge diagnose` - 诊断报告
|
||||||
|
- [ ] `wp bridge config export/import` - 配置导入导出
|
||||||
|
|
||||||
|
#### Git 仓库支持
|
||||||
|
- [x] 统一接口 SourceHandlerInterface
|
||||||
|
- [x] GitHub Releases 支持(GitHubHandler)
|
||||||
|
- [x] GitLab Releases 支持(GitLabHandler)
|
||||||
|
- [x] Gitee Releases 支持(GiteeHandler,国内)
|
||||||
|
- [x] 菲码源库支持(WenPaiGitHandler)
|
||||||
|
- [x] 私有仓库认证
|
||||||
|
|
||||||
|
#### 诊断工具
|
||||||
|
- [x] 诊断页面(源状态、请求日志)
|
||||||
|
- [x] 一键测试源连通性
|
||||||
|
- [ ] 导出诊断报告
|
||||||
|
|
||||||
|
#### 配置管理
|
||||||
|
- [ ] 导入/导出配置
|
||||||
|
- [ ] 配置备份
|
||||||
|
|
||||||
|
#### 认证支持
|
||||||
|
- [x] API Key 认证
|
||||||
|
- [x] Basic Auth 认证
|
||||||
|
- [x] 自定义 HTTP 头
|
||||||
|
|
||||||
|
#### 源优先级
|
||||||
|
- [x] 源优先级设置
|
||||||
|
- [x] 多源冲突处理(版本号最高优先)
|
||||||
|
|
||||||
|
#### FAIR 支持
|
||||||
|
- [x] FAIR Package Manager 处理器(FairHandler)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.3.0 - 源分组 + 商业插件检测 ✅
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
支持源分组管理,实现商业插件检测
|
||||||
|
|
||||||
|
### 任务清单
|
||||||
|
|
||||||
|
#### 源分组管理
|
||||||
|
- [x] 源组数据模型(GroupModel)
|
||||||
|
- [x] 批量管理界面(GroupManager)
|
||||||
|
- [x] 共享认证信息
|
||||||
|
- [x] 统一启用/禁用
|
||||||
|
|
||||||
|
#### 商业插件适配
|
||||||
|
- [x] 商业插件检测机制(CommercialDetector)
|
||||||
|
- [x] 远程 JSON 配置支持
|
||||||
|
- [x] 检测结果永久缓存
|
||||||
|
- [x] 手动刷新检测功能
|
||||||
|
- [ ] 授权验证代理(可选)
|
||||||
|
- [ ] 版本锁定功能
|
||||||
|
- [ ] 回滚机制(更新前备份)
|
||||||
|
|
||||||
|
#### 通知系统
|
||||||
|
- [x] 邮件通知(EmailHandler)
|
||||||
|
- [x] Webhook 通知(WebhookHandler)
|
||||||
|
- [ ] 更新日志聚合显示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.4.0 - Bridge API ✅
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
提供 REST API 服务,支持外部调用
|
||||||
|
|
||||||
|
### 任务清单
|
||||||
|
|
||||||
|
#### Bridge API 基础
|
||||||
|
- [x] REST API 端点设计(`/wp-json/bridge/v1/`)
|
||||||
|
- [x] 认证机制(API Key)
|
||||||
|
- [x] 状态端点 `/status`
|
||||||
|
- [ ] 限流策略
|
||||||
|
- [ ] 可用性 SLA 定义
|
||||||
|
|
||||||
|
#### API 端点
|
||||||
|
- [x] GET /bridge/v1/status - 获取状态
|
||||||
|
- [ ] GET /bridge/v1/sources - 获取可用更新源列表
|
||||||
|
- [ ] GET /bridge/v1/check/{source_id} - 检查指定源更新
|
||||||
|
- [ ] GET /bridge/v1/plugins/{slug}/info - 获取插件信息
|
||||||
|
- [ ] GET /bridge/v1/themes/{slug}/info - 获取主题信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.8.0 - 配置导入导出 + 稳定性优化 ✅
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
完善配置管理,提升稳定性
|
||||||
|
|
||||||
|
### 任务清单
|
||||||
|
|
||||||
|
#### 配置管理
|
||||||
|
- [x] 导入配置(JSON 格式)
|
||||||
|
- [x] 导出配置(JSON 格式)
|
||||||
|
- [x] 配置备份/恢复
|
||||||
|
- [x] WP-CLI 配置命令
|
||||||
|
|
||||||
|
#### 稳定性优化
|
||||||
|
- [x] 完善错误处理
|
||||||
|
- [x] 安全性检查(nonce、权限、输入清理)
|
||||||
|
- [ ] 添加单元测试(v1.0 前完成)
|
||||||
|
- [ ] 性能优化(请求去重)
|
||||||
|
- [ ] 缓存分层优化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.9.0 - 版本控制 + 用户体验 ← 当前目标
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
实现版本锁定和回滚机制,提升用户体验
|
||||||
|
|
||||||
|
### 任务清单
|
||||||
|
|
||||||
|
#### 版本锁定
|
||||||
|
- [ ] 锁定插件/主题到当前版本
|
||||||
|
- [ ] 锁定到指定版本
|
||||||
|
- [ ] 忽略特定版本更新
|
||||||
|
- [ ] 版本锁定 UI
|
||||||
|
|
||||||
|
#### 回滚机制
|
||||||
|
- [ ] 更新前自动备份
|
||||||
|
- [ ] 备份存储管理
|
||||||
|
- [ ] 一键回滚功能
|
||||||
|
- [ ] 保留最近 N 个版本
|
||||||
|
|
||||||
|
#### 更新日志聚合
|
||||||
|
- [ ] 从更新源获取 changelog
|
||||||
|
- [ ] 统一显示格式
|
||||||
|
- [ ] 在更新页面显示
|
||||||
|
|
||||||
|
#### Site Health 集成
|
||||||
|
- [ ] 更新源连通性检查
|
||||||
|
- [ ] 配置完整性检查
|
||||||
|
- [ ] 提供修复建议
|
||||||
|
|
||||||
|
#### Bridge API 完善
|
||||||
|
- [ ] GET /sources - 获取更新源列表
|
||||||
|
- [ ] GET /check/{source_id} - 检查指定源
|
||||||
|
- [ ] GET /plugins/{slug}/info - 获取插件信息
|
||||||
|
- [ ] GET /themes/{slug}/info - 获取主题信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.0.0 - 正式发布
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
稳定版本,完善文档和用户体验
|
||||||
|
|
||||||
|
### 任务清单
|
||||||
|
|
||||||
|
#### 文档完善
|
||||||
|
- [x] 用户指南
|
||||||
|
- [ ] 开发者文档(如何创建兼容的更新服务器)
|
||||||
|
- [x] API 文档
|
||||||
|
- [x] 常见问题
|
||||||
|
- [ ] 视频教程
|
||||||
|
|
||||||
|
#### 用户体验
|
||||||
|
- [ ] 状态仪表板优化
|
||||||
|
- [ ] 性能基准测试报告
|
||||||
|
|
||||||
|
#### 高级功能(延后)
|
||||||
|
- [ ] 多站点 (Multisite) 支持
|
||||||
|
- [ ] 安全扫描集成(VirusTotal/Patchstack)
|
||||||
|
- [ ] 白标/OEM 支持
|
||||||
|
|
||||||
|
#### 发布准备
|
||||||
|
- [ ] GitHub Release
|
||||||
|
- [ ] 菲码源库 Release
|
||||||
|
- [ ] 更新日志
|
||||||
|
- [ ] 宣传材料
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI 桥接层(暂缓)
|
||||||
|
|
||||||
|
> 以下功能暂缓开发,待核心功能稳定后考虑
|
||||||
|
|
||||||
|
### OpenAI 兼容层
|
||||||
|
- [ ] `pre_http_request` 拦截器
|
||||||
|
- [ ] 用户可配置白名单
|
||||||
|
- [ ] OpenAI Chat API 转发
|
||||||
|
- [ ] 响应格式转换
|
||||||
|
- [ ] 自定义端点支持(透传模式)
|
||||||
|
- [ ] WPMind 集成(可选)
|
||||||
|
|
||||||
|
### 商业插件 AI 适配
|
||||||
|
- [x] AI 网关基础(AIGateway)
|
||||||
|
- [x] Yoast SEO Pro 适配器
|
||||||
|
- [x] Rank Math 适配器
|
||||||
|
- [ ] 嗅探模式(收集 API 格式)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术架构
|
||||||
|
|
||||||
|
```
|
||||||
|
wpbridge/
|
||||||
|
├── wpbridge.php # 主文件
|
||||||
|
├── includes/
|
||||||
|
│ ├── Core/
|
||||||
|
│ │ ├── Plugin.php # 插件主类
|
||||||
|
│ │ ├── Loader.php # 自动加载
|
||||||
|
│ │ ├── Settings.php # 设置管理
|
||||||
|
│ │ ├── Logger.php # 日志系统
|
||||||
|
│ │ ├── CommercialDetector.php # 商业插件检测
|
||||||
|
│ │ ├── RemoteConfig.php # 远程配置
|
||||||
|
│ │ └── ItemSourceManager.php # 项目源管理
|
||||||
|
│ │
|
||||||
|
│ ├── UpdateSource/
|
||||||
|
│ │ ├── SourceManager.php # 更新源管理
|
||||||
|
│ │ ├── SourceModel.php # 数据模型
|
||||||
|
│ │ ├── SourceType.php # 源类型枚举
|
||||||
|
│ │ ├── PluginUpdater.php # 插件更新器
|
||||||
|
│ │ ├── ThemeUpdater.php # 主题更新器
|
||||||
|
│ │ └── Handlers/ # 各类处理器
|
||||||
|
│ │ ├── JsonHandler.php
|
||||||
|
│ │ ├── GitHubHandler.php
|
||||||
|
│ │ ├── GitLabHandler.php
|
||||||
|
│ │ ├── GiteeHandler.php
|
||||||
|
│ │ ├── ArkPressHandler.php
|
||||||
|
│ │ ├── AspireCloudHandler.php
|
||||||
|
│ │ ├── FairHandler.php
|
||||||
|
│ │ └── ...
|
||||||
|
│ │
|
||||||
|
│ ├── SourceGroup/
|
||||||
|
│ │ ├── GroupManager.php # 分组管理
|
||||||
|
│ │ └── GroupModel.php # 分组模型
|
||||||
|
│ │
|
||||||
|
│ ├── Cache/
|
||||||
|
│ │ ├── CacheManager.php # 缓存管理
|
||||||
|
│ │ ├── HealthChecker.php # 健康检查
|
||||||
|
│ │ └── FallbackStrategy.php # 降级策略
|
||||||
|
│ │
|
||||||
|
│ ├── Performance/
|
||||||
|
│ │ ├── ParallelRequestManager.php # 并行请求
|
||||||
|
│ │ ├── ConditionalRequest.php # 条件请求
|
||||||
|
│ │ └── BackgroundUpdater.php # 后台更新
|
||||||
|
│ │
|
||||||
|
│ ├── API/
|
||||||
|
│ │ ├── RestController.php # REST API
|
||||||
|
│ │ └── ApiKeyManager.php # API Key 管理
|
||||||
|
│ │
|
||||||
|
│ ├── CLI/
|
||||||
|
│ │ └── BridgeCommand.php # WP-CLI 命令
|
||||||
|
│ │
|
||||||
|
│ ├── Notification/
|
||||||
|
│ │ ├── NotificationManager.php
|
||||||
|
│ │ ├── EmailHandler.php
|
||||||
|
│ │ └── WebhookHandler.php
|
||||||
|
│ │
|
||||||
|
│ ├── AIBridge/
|
||||||
|
│ │ ├── AIGateway.php # AI 网关
|
||||||
|
│ │ └── Adapters/
|
||||||
|
│ │ ├── YoastAdapter.php
|
||||||
|
│ │ └── RankMathAdapter.php
|
||||||
|
│ │
|
||||||
|
│ └── Admin/
|
||||||
|
│ └── AdminPage.php # 管理页面
|
||||||
|
│
|
||||||
|
├── templates/
|
||||||
|
│ └── admin/
|
||||||
|
│ ├── main.php
|
||||||
|
│ └── tabs/
|
||||||
|
│ ├── overview.php
|
||||||
|
│ ├── sources.php
|
||||||
|
│ ├── diagnostics.php
|
||||||
|
│ └── api.php
|
||||||
|
│
|
||||||
|
└── assets/
|
||||||
|
├── css/
|
||||||
|
└── js/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 依赖关系
|
||||||
|
|
||||||
|
```
|
||||||
|
WPBridge 核心功能
|
||||||
|
├── 更新源桥接 - 无依赖,独立运行
|
||||||
|
├── 商业插件检测 - 无依赖,独立运行
|
||||||
|
├── Bridge API - 无依赖,独立运行
|
||||||
|
└── AI 桥接 - 可选依赖 WPMind(暂缓)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 待讨论事项
|
||||||
|
|
||||||
|
- [x] 是否需要云端配置同步?→ 暂不需要,v1.0 后考虑
|
||||||
|
- [x] 是否支持多站点?→ 付费功能(v1.0.0)
|
||||||
|
- [x] 定价策略(免费 vs 付费)?→ 基础免费 + 高级付费
|
||||||
|
- [x] 与文派叶子的集成方式?→ 独立运行,检测 WPCY 存在时官方源走 WPCY
|
||||||
|
- [x] 预置更新源?→ 文派开源(默认)、ArkPress、AspireCloud、FAIR
|
||||||
|
- [x] WP-CLI 命令前缀?→ `wp bridge`
|
||||||
|
- [x] 自托管方案支持?→ ArkPress、AspireCloud、UpdatePulse Server、PUC 格式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*最后更新: 2026-02-05*
|
||||||
2969
assets/css/admin.css
Normal file
2969
assets/css/admin.css
Normal file
File diff suppressed because it is too large
Load diff
2675
assets/js/admin.js
Normal file
2675
assets/js/admin.js
Normal file
File diff suppressed because it is too large
Load diff
73
examples/commercial-config.json
Normal file
73
examples/commercial-config.json
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"updated_at": "2026-02-05",
|
||||||
|
"description": "WPBridge 商业插件检测配置文件 - 由 wpcy.com 维护",
|
||||||
|
"commercial_plugins": [
|
||||||
|
"elementor-pro",
|
||||||
|
"wordpress-seo-premium",
|
||||||
|
"yoast-seo-premium",
|
||||||
|
"wpforms-pro",
|
||||||
|
"gravityforms",
|
||||||
|
"advanced-custom-fields-pro",
|
||||||
|
"acf-pro",
|
||||||
|
"woocommerce-subscriptions",
|
||||||
|
"woocommerce-memberships",
|
||||||
|
"woocommerce-bookings",
|
||||||
|
"sitepress-multilingual-cms",
|
||||||
|
"wpml-string-translation",
|
||||||
|
"updraftplus-premium",
|
||||||
|
"seo-by-rank-math-pro",
|
||||||
|
"monsterinsights-pro",
|
||||||
|
"optinmonster-pro",
|
||||||
|
"wp-rocket",
|
||||||
|
"perfmatters",
|
||||||
|
"wp-smush-pro",
|
||||||
|
"wordfence-premium",
|
||||||
|
"ithemes-security-pro",
|
||||||
|
"backupbuddy",
|
||||||
|
"bb-plugin",
|
||||||
|
"divi-builder",
|
||||||
|
"brizy-pro",
|
||||||
|
"sfwd-lms",
|
||||||
|
"memberpress",
|
||||||
|
"restrict-content-pro",
|
||||||
|
"affiliate-wp",
|
||||||
|
"easy-digital-downloads-pro",
|
||||||
|
"ninja-forms-pro",
|
||||||
|
"formidable-pro",
|
||||||
|
"fluentformpro",
|
||||||
|
"wp-all-import-pro",
|
||||||
|
"searchwp",
|
||||||
|
"facetwp",
|
||||||
|
"admin-columns-pro"
|
||||||
|
],
|
||||||
|
"commercial_domains": [
|
||||||
|
"codecanyon.net",
|
||||||
|
"themeforest.net",
|
||||||
|
"elegantthemes.com",
|
||||||
|
"developer.yoast.com",
|
||||||
|
"developer.wpforms.com",
|
||||||
|
"developer.monsterinsights.com",
|
||||||
|
"developer.optinmonster.com",
|
||||||
|
"developer.seedprod.com",
|
||||||
|
"developer.ithemes.com"
|
||||||
|
],
|
||||||
|
"license_keywords": [
|
||||||
|
"license_key",
|
||||||
|
"license_status",
|
||||||
|
"activate_license",
|
||||||
|
"deactivate_license",
|
||||||
|
"check_license",
|
||||||
|
"is_valid_license",
|
||||||
|
"license_page",
|
||||||
|
"enter your license",
|
||||||
|
"purchase_code",
|
||||||
|
"activation_key"
|
||||||
|
],
|
||||||
|
"commercial_frameworks": [
|
||||||
|
"EDD_SL_Plugin_Updater",
|
||||||
|
"Freemius",
|
||||||
|
"WC_AM_Client",
|
||||||
|
"Starter_Plugin_Updater"
|
||||||
|
]
|
||||||
|
}
|
||||||
354
includes/AIBridge/AIGateway.php
Normal file
354
includes/AIBridge/AIGateway.php
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AI 网关
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\AIBridge;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Security\Validator;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 网关类
|
||||||
|
* 拦截并转发 AI API 请求
|
||||||
|
*/
|
||||||
|
class AIGateway {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已注册的适配器
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $adapters = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 白名单域名
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $whitelist = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
*/
|
||||||
|
public function __construct( Settings $settings ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->whitelist = $this->get_whitelist();
|
||||||
|
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks(): void {
|
||||||
|
// 只有启用 AI 桥接时才注册钩子
|
||||||
|
if ( ! $this->is_enabled() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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', [] );
|
||||||
|
return ! empty( $ai_settings['enabled'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取桥接模式
|
||||||
|
*
|
||||||
|
* @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', [] );
|
||||||
|
$whitelist = $ai_settings['whitelist'] ?? [];
|
||||||
|
|
||||||
|
// 默认白名单
|
||||||
|
$default_whitelist = [
|
||||||
|
'api.openai.com',
|
||||||
|
'api.anthropic.com',
|
||||||
|
'generativelanguage.googleapis.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是 AI API 请求
|
||||||
|
if ( ! $this->is_ai_request( $url ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::debug( 'AI 请求拦截', [ 'url' => $url ] );
|
||||||
|
|
||||||
|
// 根据模式处理
|
||||||
|
$mode = $this->get_mode();
|
||||||
|
|
||||||
|
switch ( $mode ) {
|
||||||
|
case 'passthrough':
|
||||||
|
return $this->handle_passthrough( $url, $args );
|
||||||
|
|
||||||
|
case 'wpmind':
|
||||||
|
return $this->handle_wpmind( $url, $args );
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
if ( empty( $host ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = strtolower( $host );
|
||||||
|
|
||||||
|
foreach ( $this->whitelist as $allowed ) {
|
||||||
|
if ( strtolower( $allowed ) === $host ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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', [] );
|
||||||
|
$custom_endpoint = $ai_settings['custom_endpoint'] ?? '';
|
||||||
|
|
||||||
|
if ( empty( $custom_endpoint ) ) {
|
||||||
|
Logger::warning( '透传模式未配置自定义端点' );
|
||||||
|
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 );
|
||||||
|
|
||||||
|
Logger::debug( 'AI 请求透传', [
|
||||||
|
'original' => $url,
|
||||||
|
'new' => $new_url,
|
||||||
|
] );
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
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 API
|
||||||
|
$wpmind_endpoint = apply_filters( 'wpmind_api_endpoint', 'https://api.wpmind.cn/v1' );
|
||||||
|
|
||||||
|
// 转换请求格式
|
||||||
|
$converted_args = $this->convert_to_wpmind_format( $url, $args );
|
||||||
|
|
||||||
|
Logger::debug( 'AI 请求转发到 WPMind', [
|
||||||
|
'original' => $url,
|
||||||
|
'endpoint' => $wpmind_endpoint,
|
||||||
|
] );
|
||||||
|
|
||||||
|
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' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换端点
|
||||||
|
*
|
||||||
|
* @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, '/' );
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新认证头
|
||||||
|
if ( ! empty( $api_key ) ) {
|
||||||
|
$args['headers']['Authorization'] = 'Bearer ' . $api_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加来源标识
|
||||||
|
$args['headers']['X-WPBridge-Source'] = 'wpbridge';
|
||||||
|
|
||||||
|
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', [ $this, 'intercept_request' ], 10 );
|
||||||
|
|
||||||
|
$response = wp_remote_request( $url, $args );
|
||||||
|
|
||||||
|
// 重新添加过滤器
|
||||||
|
add_filter( 'pre_http_request', [ $this, 'intercept_request' ], 10, 3 );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
Logger::error( 'AI 请求转发失败', [
|
||||||
|
'url' => $url,
|
||||||
|
'error' => $response->get_error_message(),
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
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 适配器', [ 'name' => $name ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取适配器
|
||||||
|
*
|
||||||
|
* @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_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 ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
172
includes/AIBridge/Adapters/AbstractAdapter.php
Normal file
172
includes/AIBridge/Adapters/AbstractAdapter.php
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AI 适配器抽象基类
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\AIBridge\Adapters;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 适配器抽象基类
|
||||||
|
*/
|
||||||
|
abstract class AbstractAdapter implements AdapterInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
protected Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的插件 slug 列表
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected array $supported_plugins = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配的 URL 模式
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected array $url_patterns = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @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 $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', [] );
|
||||||
|
$adapters = $ai_settings['adapters'] ?? [];
|
||||||
|
|
||||||
|
return in_array( $this->get_name(), $adapters, true );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录日志
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = $args['body'];
|
||||||
|
|
||||||
|
if ( is_string( $body ) ) {
|
||||||
|
$decoded = json_decode( $body, true );
|
||||||
|
return json_last_error() === JSON_ERROR_NONE ? $decoded : 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|\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 );
|
||||||
|
|
||||||
|
if ( empty( $body ) ) {
|
||||||
|
return 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
74
includes/AIBridge/Adapters/AdapterInterface.php
Normal file
74
includes/AIBridge/Adapters/AdapterInterface.php
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AI 适配器接口
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\AIBridge\Adapters;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 适配器接口
|
||||||
|
*/
|
||||||
|
interface AdapterInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取适配器名称
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_name(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取适配器描述
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_description(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否支持该插件
|
||||||
|
*
|
||||||
|
* @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 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 );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function is_enabled(): bool;
|
||||||
|
}
|
||||||
185
includes/AIBridge/Adapters/RankMathAdapter.php
Normal file
185
includes/AIBridge/Adapters/RankMathAdapter.php
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Rank Math AI 适配器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\AIBridge\Adapters;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rank Math AI 适配器类
|
||||||
|
* 适配 Rank Math SEO 的 Content AI 功能
|
||||||
|
*/
|
||||||
|
class RankMathAdapter extends AbstractAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的插件
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected array $supported_plugins = [
|
||||||
|
'seo-by-rank-math',
|
||||||
|
'seo-by-rank-math-pro',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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_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 );
|
||||||
|
|
||||||
|
if ( null === $body ) {
|
||||||
|
return [ $url, $args ];
|
||||||
|
}
|
||||||
|
|
||||||
|
$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'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据请求类型优化
|
||||||
|
$request_type = $this->detect_request_type( $body );
|
||||||
|
$body = $this->optimize_for_type( $body, $request_type );
|
||||||
|
|
||||||
|
$args = $this->set_request_body( $args, $body );
|
||||||
|
|
||||||
|
return [ $url, $args ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换响应
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
|
||||||
|
if ( null === $body ) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log( 'Rank Math AI 响应转换', [
|
||||||
|
'has_choices' => isset( $body['choices'] ),
|
||||||
|
] );
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = '';
|
||||||
|
foreach ( $body['messages'] as $message ) {
|
||||||
|
if ( isset( $message['content'] ) ) {
|
||||||
|
$content .= $message['content'] . ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = strtolower( $content );
|
||||||
|
|
||||||
|
if ( strpos( $content, 'title' ) !== false || strpos( $content, '标题' ) !== false ) {
|
||||||
|
return 'title';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( strpos( $content, 'description' ) !== false || strpos( $content, '描述' ) !== false ) {
|
||||||
|
return 'description';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( strpos( $content, 'keyword' ) !== false || strpos( $content, '关键词' ) !== false ) {
|
||||||
|
return 'keyword';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( strpos( $content, 'content' ) !== false || strpos( $content, '内容' ) !== false ) {
|
||||||
|
return 'content';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'general';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型优化请求
|
||||||
|
*
|
||||||
|
* @param array $body 请求体
|
||||||
|
* @param string $type 请求类型
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function optimize_for_type( array $body, string $type ): array {
|
||||||
|
// 可以根据不同类型添加优化逻辑
|
||||||
|
// 例如:为中文内容生成添加特定提示
|
||||||
|
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射模型名称
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
145
includes/AIBridge/Adapters/YoastAdapter.php
Normal file
145
includes/AIBridge/Adapters/YoastAdapter.php
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Yoast SEO AI 适配器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\AIBridge\Adapters;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yoast SEO AI 适配器类
|
||||||
|
* 适配 Yoast SEO Premium 的 AI 功能
|
||||||
|
*/
|
||||||
|
class YoastAdapter extends AbstractAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的插件
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected array $supported_plugins = [
|
||||||
|
'wordpress-seo-premium',
|
||||||
|
'wordpress-seo',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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_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 );
|
||||||
|
|
||||||
|
if ( null === $body ) {
|
||||||
|
return [ $url, $args ];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log( 'Yoast AI 请求转换', [
|
||||||
|
'model' => $body['model'] ?? 'unknown',
|
||||||
|
] );
|
||||||
|
|
||||||
|
// 转换模型名称(如果需要)
|
||||||
|
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'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = $this->set_request_body( $args, $body );
|
||||||
|
|
||||||
|
return [ $url, $args ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换响应
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
|
||||||
|
if ( null === $body ) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log( 'Yoast AI 响应转换', [
|
||||||
|
'has_choices' => isset( $body['choices'] ),
|
||||||
|
] );
|
||||||
|
|
||||||
|
// 响应格式通常兼容,无需转换
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射模型名称
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优化消息
|
||||||
|
*
|
||||||
|
* @param array $messages 消息列表
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function optimize_messages( array $messages ): array {
|
||||||
|
// 可以在这里添加中文优化提示
|
||||||
|
// 例如:添加系统消息要求返回中文内容
|
||||||
|
|
||||||
|
return $messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
287
includes/API/ApiKeyManager.php
Normal file
287
includes/API/ApiKeyManager.php
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* API Key 管理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\API;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Security\Encryption;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Key 管理器类
|
||||||
|
*/
|
||||||
|
class ApiKeyManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private 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 {
|
||||||
|
// 权限检查
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
throw new \Exception( __( '权限不足', 'wpbridge' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_key = Encryption::generate_token( 32 );
|
||||||
|
$key_id = 'key_' . wp_generate_uuid4();
|
||||||
|
|
||||||
|
$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', [] );
|
||||||
|
$api_settings['keys'] = $api_settings['keys'] ?? [];
|
||||||
|
$api_settings['keys'][] = $key_data;
|
||||||
|
|
||||||
|
$this->settings->set( 'api', $api_settings );
|
||||||
|
|
||||||
|
Logger::info( 'API Key 已创建', [ 'id' => $key_id, 'name' => $name ] );
|
||||||
|
|
||||||
|
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', [] );
|
||||||
|
$keys = $api_settings['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', [] );
|
||||||
|
$keys = $api_settings['keys'] ?? [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( count( $new_keys ) === count( $keys ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_settings['keys'] = $new_keys;
|
||||||
|
$this->settings->set( 'api', $api_settings );
|
||||||
|
|
||||||
|
Logger::info( 'API Key 已删除', [ 'id' => $key_id ] );
|
||||||
|
|
||||||
|
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', [] );
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $found ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_settings['keys'] = $keys;
|
||||||
|
$this->settings->set( 'api', $api_settings );
|
||||||
|
|
||||||
|
Logger::info( 'API Key 已更新', [ 'id' => $key_id ] );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtotime( $key['expires_at'] ) < time();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销所有 API Keys
|
||||||
|
*
|
||||||
|
* @return int 撤销的数量
|
||||||
|
*/
|
||||||
|
public function revoke_all(): int {
|
||||||
|
$api_settings = $this->settings->get( 'api', [] );
|
||||||
|
$count = count( $api_settings['keys'] ?? [] );
|
||||||
|
|
||||||
|
$api_settings['keys'] = [];
|
||||||
|
$this->settings->set( 'api', $api_settings );
|
||||||
|
|
||||||
|
Logger::info( '所有 API Keys 已撤销', [ 'count' => $count ] );
|
||||||
|
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统计信息
|
||||||
|
*
|
||||||
|
* @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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => $total,
|
||||||
|
'active' => $active,
|
||||||
|
'expired' => $expired,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
751
includes/API/RestController.php
Normal file
751
includes/API/RestController.php
Normal file
|
|
@ -0,0 +1,751 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* REST API 控制器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\API;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\UpdateSource\SourceManager;
|
||||||
|
use WPBridge\Cache\CacheManager;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API 控制器类
|
||||||
|
*/
|
||||||
|
class RestController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 命名空间
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const NAMESPACE = 'bridge/v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源管理器
|
||||||
|
*
|
||||||
|
* @var SourceManager
|
||||||
|
*/
|
||||||
|
private SourceManager $source_manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理器
|
||||||
|
*
|
||||||
|
* @var CacheManager
|
||||||
|
*/
|
||||||
|
private CacheManager $cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
*/
|
||||||
|
public function __construct( Settings $settings ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->source_manager = new SourceManager( $settings );
|
||||||
|
$this->cache = new CacheManager();
|
||||||
|
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks(): void {
|
||||||
|
add_action( 'rest_api_init', [ $this, 'register_routes' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册路由
|
||||||
|
*/
|
||||||
|
public function register_routes(): void {
|
||||||
|
// 获取所有更新源
|
||||||
|
register_rest_route( self::NAMESPACE, '/sources', [
|
||||||
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
|
'callback' => [ $this, 'get_sources' ],
|
||||||
|
'permission_callback' => [ $this, 'check_api_permission' ],
|
||||||
|
] );
|
||||||
|
|
||||||
|
// 获取单个更新源
|
||||||
|
register_rest_route( self::NAMESPACE, '/sources/(?P<id>[a-zA-Z0-9_-]+)', [
|
||||||
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
|
'callback' => [ $this, 'get_source' ],
|
||||||
|
'permission_callback' => [ $this, 'check_api_permission' ],
|
||||||
|
'args' => [
|
||||||
|
'id' => [
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
// 检查更新源状态
|
||||||
|
register_rest_route( self::NAMESPACE, '/check/(?P<source_id>[a-zA-Z0-9_-]+)', [
|
||||||
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
|
'callback' => [ $this, 'check_source' ],
|
||||||
|
'permission_callback' => [ $this, 'check_api_permission' ],
|
||||||
|
'args' => [
|
||||||
|
'source_id' => [
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
// 获取插件信息
|
||||||
|
register_rest_route( self::NAMESPACE, '/plugins/(?P<slug>[a-z0-9-]+)/info', [
|
||||||
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
|
'callback' => [ $this, 'get_plugin_info' ],
|
||||||
|
'permission_callback' => [ $this, 'check_api_permission' ],
|
||||||
|
'args' => [
|
||||||
|
'slug' => [
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_title',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
// 获取主题信息
|
||||||
|
register_rest_route( self::NAMESPACE, '/themes/(?P<slug>[a-z0-9-]+)/info', [
|
||||||
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
|
'callback' => [ $this, 'get_theme_info' ],
|
||||||
|
'permission_callback' => [ $this, 'check_api_permission' ],
|
||||||
|
'args' => [
|
||||||
|
'slug' => [
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_title',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
// 菲码源库 Releases
|
||||||
|
register_rest_route( self::NAMESPACE, '/wenpai-git/(?P<repo>[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+)/releases', [
|
||||||
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
|
'callback' => [ $this, 'get_wenpai_git_releases' ],
|
||||||
|
'permission_callback' => [ $this, 'check_api_permission' ],
|
||||||
|
'args' => [
|
||||||
|
'repo' => [
|
||||||
|
'required' => true,
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'validate_callback' => function ( $param ) {
|
||||||
|
return preg_match( '/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/', $param );
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
// API 状态
|
||||||
|
register_rest_route( self::NAMESPACE, '/status', [
|
||||||
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
|
'callback' => [ $this, 'get_status' ],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 API 权限
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return bool|\WP_Error
|
||||||
|
*/
|
||||||
|
public function check_api_permission( \WP_REST_Request $request ) {
|
||||||
|
$api_settings = $this->settings->get( 'api', [] );
|
||||||
|
|
||||||
|
// 检查 API 是否启用
|
||||||
|
if ( empty( $api_settings['enabled'] ) ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'api_disabled',
|
||||||
|
__( 'API 未启用', 'wpbridge' ),
|
||||||
|
[ 'status' => 403 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要认证
|
||||||
|
if ( ! empty( $api_settings['require_auth'] ) ) {
|
||||||
|
$api_key = $this->get_api_key_from_request( $request );
|
||||||
|
|
||||||
|
if ( empty( $api_key ) ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'missing_api_key',
|
||||||
|
__( '缺少 API Key', 'wpbridge' ),
|
||||||
|
[ 'status' => 401 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $this->validate_api_key( $api_key ) ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'invalid_api_key',
|
||||||
|
__( '无效的 API Key', 'wpbridge' ),
|
||||||
|
[ 'status' => 401 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查速率限制
|
||||||
|
$rate_limit_result = $this->check_rate_limit( $request );
|
||||||
|
if ( is_wp_error( $rate_limit_result ) ) {
|
||||||
|
return $rate_limit_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从请求中获取 API Key
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function get_api_key_from_request( \WP_REST_Request $request ): string {
|
||||||
|
// 优先从 Header 获取
|
||||||
|
$auth_header = $request->get_header( 'X-WPBridge-API-Key' );
|
||||||
|
if ( ! empty( $auth_header ) ) {
|
||||||
|
return sanitize_text_field( $auth_header );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Authorization Bearer 获取
|
||||||
|
$auth_header = $request->get_header( 'Authorization' );
|
||||||
|
if ( ! empty( $auth_header ) && strpos( $auth_header, 'Bearer ' ) === 0 ) {
|
||||||
|
return sanitize_text_field( substr( $auth_header, 7 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从查询参数获取(不推荐,记录警告)
|
||||||
|
$api_key = $request->get_param( 'api_key' );
|
||||||
|
if ( ! empty( $api_key ) ) {
|
||||||
|
Logger::warning( 'API Key 通过 URL 参数传递,建议使用 Header 方式', [
|
||||||
|
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||||
|
] );
|
||||||
|
return sanitize_text_field( $api_key );
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 API Key
|
||||||
|
*
|
||||||
|
* @param string $api_key API Key
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function validate_api_key( string $api_key ): bool {
|
||||||
|
$api_settings = $this->settings->get( 'api', [] );
|
||||||
|
$valid_keys = $api_settings['keys'] ?? [];
|
||||||
|
|
||||||
|
foreach ( $valid_keys as $key_data ) {
|
||||||
|
// 使用 password_verify 验证哈希
|
||||||
|
if ( isset( $key_data['key_hash'] ) && password_verify( $api_key, $key_data['key_hash'] ) ) {
|
||||||
|
// 检查是否过期
|
||||||
|
if ( ! empty( $key_data['expires_at'] ) ) {
|
||||||
|
if ( strtotime( $key_data['expires_at'] ) < time() ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录使用(使用缓存批量更新)
|
||||||
|
$this->record_api_key_usage( $key_data['id'] );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 API Key 使用
|
||||||
|
*
|
||||||
|
* @param string $key_id Key ID
|
||||||
|
*/
|
||||||
|
private function record_api_key_usage( string $key_id ): void {
|
||||||
|
// 使用缓存记录,避免频繁写入数据库
|
||||||
|
$cache_key = 'api_usage_' . $key_id;
|
||||||
|
$count = $this->cache->get( $cache_key );
|
||||||
|
|
||||||
|
if ( false === $count ) {
|
||||||
|
$count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count++;
|
||||||
|
$this->cache->set( $cache_key, $count, 300 );
|
||||||
|
|
||||||
|
// 每 50 次批量写入数据库
|
||||||
|
if ( $count >= 50 ) {
|
||||||
|
$this->flush_usage_to_db( $key_id, $count );
|
||||||
|
$this->cache->set( $cache_key, 0, 300 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将使用统计写入数据库
|
||||||
|
*
|
||||||
|
* @param string $key_id Key ID
|
||||||
|
* @param int $count 使用次数
|
||||||
|
*/
|
||||||
|
private function flush_usage_to_db( string $key_id, int $count ): void {
|
||||||
|
$api_settings = $this->settings->get( 'api', [] );
|
||||||
|
$keys = $api_settings['keys'] ?? [];
|
||||||
|
|
||||||
|
foreach ( $keys as $index => $key ) {
|
||||||
|
if ( $key['id'] === $key_id ) {
|
||||||
|
$keys[ $index ]['last_used'] = current_time( 'mysql' );
|
||||||
|
$keys[ $index ]['usage_count'] = ( $key['usage_count'] ?? 0 ) + $count;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_settings['keys'] = $keys;
|
||||||
|
$this->settings->set( 'api', $api_settings );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查速率限制
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return true|\WP_Error
|
||||||
|
*/
|
||||||
|
private function check_rate_limit( \WP_REST_Request $request ) {
|
||||||
|
$api_settings = $this->settings->get( 'api', [] );
|
||||||
|
$rate_limit = $api_settings['rate_limit'] ?? 60; // 默认每分钟 60 次
|
||||||
|
|
||||||
|
if ( $rate_limit <= 0 ) {
|
||||||
|
return true; // 无限制
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取客户端标识
|
||||||
|
$client_id = $this->get_client_identifier( $request );
|
||||||
|
$cache_key = 'rate_limit_' . md5( $client_id );
|
||||||
|
|
||||||
|
$current = $this->cache->get( $cache_key );
|
||||||
|
|
||||||
|
if ( false === $current ) {
|
||||||
|
$this->cache->set( $cache_key, 1, 60 );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $current >= $rate_limit ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'rate_limit_exceeded',
|
||||||
|
__( '请求过于频繁,请稍后再试', 'wpbridge' ),
|
||||||
|
[
|
||||||
|
'status' => 429,
|
||||||
|
'retry_after' => 60,
|
||||||
|
'x-ratelimit-limit' => $rate_limit,
|
||||||
|
'x-ratelimit-remaining' => 0,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cache->set( $cache_key, $current + 1, 60 );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端标识
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function get_client_identifier( \WP_REST_Request $request ): string {
|
||||||
|
// 优先使用 API Key
|
||||||
|
$api_key = $this->get_api_key_from_request( $request );
|
||||||
|
if ( ! empty( $api_key ) ) {
|
||||||
|
return 'key:' . md5( $api_key );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 IP 地址
|
||||||
|
return 'ip:' . $this->get_client_ip( $request );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端 IP 地址
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function get_client_ip( \WP_REST_Request $request ): string {
|
||||||
|
// 检查是否配置了可信代理
|
||||||
|
$trusted_proxies = apply_filters( 'wpbridge_trusted_proxies', [] );
|
||||||
|
|
||||||
|
if ( ! empty( $trusted_proxies ) ) {
|
||||||
|
$remote_addr = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||||
|
|
||||||
|
// 只有当请求来自可信代理时才信任 X-Forwarded-For
|
||||||
|
if ( in_array( $remote_addr, $trusted_proxies, true ) ) {
|
||||||
|
$forwarded = $request->get_header( 'X-Forwarded-For' );
|
||||||
|
if ( ! empty( $forwarded ) ) {
|
||||||
|
// 取第一个非代理 IP
|
||||||
|
$ips = array_map( 'trim', explode( ',', $forwarded ) );
|
||||||
|
return sanitize_text_field( $ips[0] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitize_text_field( $_SERVER['REMOTE_ADDR'] ?? 'unknown' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有更新源
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return \WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function get_sources( \WP_REST_Request $request ): \WP_REST_Response {
|
||||||
|
$sources = $this->source_manager->get_enabled_sorted();
|
||||||
|
|
||||||
|
$data = array_map( function ( $source ) {
|
||||||
|
return [
|
||||||
|
'id' => $source->id,
|
||||||
|
'name' => $source->name,
|
||||||
|
'type' => $source->type,
|
||||||
|
'item_type' => $source->item_type,
|
||||||
|
'slug' => $source->slug,
|
||||||
|
'enabled' => $source->enabled,
|
||||||
|
'priority' => $source->priority,
|
||||||
|
];
|
||||||
|
}, $sources );
|
||||||
|
|
||||||
|
return new \WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'data' => array_values( $data ),
|
||||||
|
'total' => count( $data ),
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个更新源
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return \WP_REST_Response|\WP_Error
|
||||||
|
*/
|
||||||
|
public function get_source( \WP_REST_Request $request ) {
|
||||||
|
$id = $request->get_param( 'id' );
|
||||||
|
$source = $this->source_manager->get( $id );
|
||||||
|
|
||||||
|
if ( null === $source ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'source_not_found',
|
||||||
|
__( '更新源不存在', 'wpbridge' ),
|
||||||
|
[ 'status' => 404 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new \WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'id' => $source->id,
|
||||||
|
'name' => $source->name,
|
||||||
|
'type' => $source->type,
|
||||||
|
'api_url' => $source->api_url,
|
||||||
|
'item_type' => $source->item_type,
|
||||||
|
'slug' => $source->slug,
|
||||||
|
'enabled' => $source->enabled,
|
||||||
|
'priority' => $source->priority,
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查更新源状态
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return \WP_REST_Response|\WP_Error
|
||||||
|
*/
|
||||||
|
public function check_source( \WP_REST_Request $request ) {
|
||||||
|
$source_id = $request->get_param( 'source_id' );
|
||||||
|
$source = $this->source_manager->get( $source_id );
|
||||||
|
|
||||||
|
if ( null === $source ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'source_not_found',
|
||||||
|
__( '更新源不存在', 'wpbridge' ),
|
||||||
|
[ 'status' => 404 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$checker = new \WPBridge\Cache\HealthChecker();
|
||||||
|
$status = $checker->check( $source, true );
|
||||||
|
|
||||||
|
return new \WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'source_id' => $source_id,
|
||||||
|
'status' => $status->status,
|
||||||
|
'response_time' => $status->response_time,
|
||||||
|
'error' => $status->error,
|
||||||
|
'checked_at' => current_time( 'c' ),
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件信息
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return \WP_REST_Response|\WP_Error
|
||||||
|
*/
|
||||||
|
public function get_plugin_info( \WP_REST_Request $request ) {
|
||||||
|
$slug = $request->get_param( 'slug' );
|
||||||
|
|
||||||
|
// 从缓存获取
|
||||||
|
$cache_key = 'plugin_info_' . $slug;
|
||||||
|
$cached = $this->cache->get( $cache_key );
|
||||||
|
|
||||||
|
if ( false !== $cached ) {
|
||||||
|
return new \WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $cached,
|
||||||
|
'cached' => true,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找匹配的更新源
|
||||||
|
$sources = $this->source_manager->get_enabled_sorted();
|
||||||
|
$info = null;
|
||||||
|
|
||||||
|
foreach ( $sources as $source ) {
|
||||||
|
if ( $source->item_type !== 'plugin' ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $source->slug ) && $source->slug !== $slug ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handler = $source->get_handler();
|
||||||
|
if ( null === $handler ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$info = $handler->get_info( $slug );
|
||||||
|
if ( null !== $info ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
Logger::debug( '获取插件信息失败', [
|
||||||
|
'slug' => $slug,
|
||||||
|
'source' => $source->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( null === $info ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'plugin_not_found',
|
||||||
|
__( '未找到插件信息', 'wpbridge' ),
|
||||||
|
[ 'status' => 404 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
$this->cache->set( $cache_key, $info, $this->settings->get_cache_ttl() );
|
||||||
|
|
||||||
|
return new \WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $info,
|
||||||
|
'cached' => false,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主题信息
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return \WP_REST_Response|\WP_Error
|
||||||
|
*/
|
||||||
|
public function get_theme_info( \WP_REST_Request $request ) {
|
||||||
|
$slug = $request->get_param( 'slug' );
|
||||||
|
|
||||||
|
// 从缓存获取
|
||||||
|
$cache_key = 'theme_info_' . $slug;
|
||||||
|
$cached = $this->cache->get( $cache_key );
|
||||||
|
|
||||||
|
if ( false !== $cached ) {
|
||||||
|
return new \WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $cached,
|
||||||
|
'cached' => true,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找匹配的更新源
|
||||||
|
$sources = $this->source_manager->get_enabled_sorted();
|
||||||
|
$info = null;
|
||||||
|
|
||||||
|
foreach ( $sources as $source ) {
|
||||||
|
if ( $source->item_type !== 'theme' ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $source->slug ) && $source->slug !== $slug ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handler = $source->get_handler();
|
||||||
|
if ( null === $handler ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$info = $handler->get_info( $slug );
|
||||||
|
if ( null !== $info ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
Logger::debug( '获取主题信息失败', [
|
||||||
|
'slug' => $slug,
|
||||||
|
'source' => $source->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( null === $info ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'theme_not_found',
|
||||||
|
__( '未找到主题信息', 'wpbridge' ),
|
||||||
|
[ 'status' => 404 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
$this->cache->set( $cache_key, $info, $this->settings->get_cache_ttl() );
|
||||||
|
|
||||||
|
return new \WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $info,
|
||||||
|
'cached' => false,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取菲码源库 Releases
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return \WP_REST_Response|\WP_Error
|
||||||
|
*/
|
||||||
|
public function get_wenpai_git_releases( \WP_REST_Request $request ) {
|
||||||
|
$repo = $request->get_param( 'repo' );
|
||||||
|
|
||||||
|
// 从缓存获取
|
||||||
|
$cache_key = 'wenpai_git_' . md5( $repo );
|
||||||
|
$cached = $this->cache->get( $cache_key );
|
||||||
|
|
||||||
|
if ( false !== $cached ) {
|
||||||
|
return new \WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $cached,
|
||||||
|
'cached' => true,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用菲码源库 API
|
||||||
|
$api_url = 'https://git.wenpai.org/api/v1/repos/' . $repo . '/releases';
|
||||||
|
$response = wp_remote_get( $api_url, [
|
||||||
|
'timeout' => $this->settings->get_request_timeout(),
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'api_error',
|
||||||
|
$response->get_error_message(),
|
||||||
|
[ 'status' => 502 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status_code = wp_remote_retrieve_response_code( $response );
|
||||||
|
if ( $status_code !== 200 ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'api_error',
|
||||||
|
sprintf( __( '菲码源库 API 返回错误: %d', 'wpbridge' ), $status_code ),
|
||||||
|
[ 'status' => $status_code ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
$data = json_decode( $body, true );
|
||||||
|
|
||||||
|
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||||
|
return new \WP_Error(
|
||||||
|
'json_error',
|
||||||
|
__( 'JSON 解析失败', 'wpbridge' ),
|
||||||
|
[ 'status' => 500 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化数据
|
||||||
|
$releases = array_map( function ( $release ) {
|
||||||
|
return [
|
||||||
|
'id' => $release['id'] ?? 0,
|
||||||
|
'tag_name' => $release['tag_name'] ?? '',
|
||||||
|
'name' => $release['name'] ?? '',
|
||||||
|
'body' => $release['body'] ?? '',
|
||||||
|
'draft' => $release['draft'] ?? false,
|
||||||
|
'prerelease' => $release['prerelease'] ?? false,
|
||||||
|
'created_at' => $release['created_at'] ?? '',
|
||||||
|
'published_at' => $release['published_at'] ?? '',
|
||||||
|
'assets' => array_map( function ( $asset ) {
|
||||||
|
return [
|
||||||
|
'name' => $asset['name'] ?? '',
|
||||||
|
'size' => $asset['size'] ?? 0,
|
||||||
|
'download_url' => $asset['browser_download_url'] ?? '',
|
||||||
|
'download_count' => $asset['download_count'] ?? 0,
|
||||||
|
];
|
||||||
|
}, $release['assets'] ?? [] ),
|
||||||
|
];
|
||||||
|
}, $data );
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
$this->cache->set( $cache_key, $releases, $this->settings->get_cache_ttl() );
|
||||||
|
|
||||||
|
return new \WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $releases,
|
||||||
|
'cached' => false,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 API 状态
|
||||||
|
*
|
||||||
|
* @param \WP_REST_Request $request 请求对象
|
||||||
|
* @return \WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function get_status( \WP_REST_Request $request ): \WP_REST_Response {
|
||||||
|
return new \WP_REST_Response( [
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'version' => WPBRIDGE_VERSION,
|
||||||
|
'api_version' => 'v1',
|
||||||
|
'endpoints' => [
|
||||||
|
'sources' => rest_url( self::NAMESPACE . '/sources' ),
|
||||||
|
'check' => rest_url( self::NAMESPACE . '/check/{source_id}' ),
|
||||||
|
'plugins' => rest_url( self::NAMESPACE . '/plugins/{slug}/info' ),
|
||||||
|
'themes' => rest_url( self::NAMESPACE . '/themes/{slug}/info' ),
|
||||||
|
'wenpai_git' => rest_url( self::NAMESPACE . '/wenpai-git/{repo}/releases' ),
|
||||||
|
],
|
||||||
|
'timestamp' => current_time( 'c' ),
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
1004
includes/Admin/AdminPage.php
Normal file
1004
includes/Admin/AdminPage.php
Normal file
File diff suppressed because it is too large
Load diff
401
includes/Admin/VendorAdmin.php
Normal file
401
includes/Admin/VendorAdmin.php
Normal file
|
|
@ -0,0 +1,401 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 供应商管理后台
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.8
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WPBridge\Admin;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Commercial\BridgeManager;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VendorAdmin 类
|
||||||
|
*/
|
||||||
|
class VendorAdmin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 桥接管理器
|
||||||
|
*
|
||||||
|
* @var BridgeManager|null
|
||||||
|
*/
|
||||||
|
private ?BridgeManager $bridge_manager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
*/
|
||||||
|
public function __construct( Settings $settings ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function init_hooks(): void {
|
||||||
|
// 供应商 AJAX 处理
|
||||||
|
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', [ $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', [ $this, 'ajax_test_bridge_server' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取桥接管理器
|
||||||
|
*
|
||||||
|
* @return BridgeManager
|
||||||
|
*/
|
||||||
|
private function get_bridge_manager(): BridgeManager {
|
||||||
|
if ( null === $this->bridge_manager ) {
|
||||||
|
$remote_config = new \WPBridge\Core\RemoteConfig( $this->settings );
|
||||||
|
$this->bridge_manager = new BridgeManager( $this->settings, $remote_config );
|
||||||
|
}
|
||||||
|
return $this->bridge_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 添加供应商
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_add_vendor(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
|
||||||
|
$name = sanitize_text_field( $_POST['name'] ?? '' );
|
||||||
|
$type = sanitize_text_field( $_POST['type'] ?? 'woocommerce' );
|
||||||
|
$api_url = esc_url_raw( $_POST['api_url'] ?? '' );
|
||||||
|
$consumer_key = sanitize_text_field( $_POST['consumer_key'] ?? '' );
|
||||||
|
$consumer_secret = sanitize_text_field( $_POST['consumer_secret'] ?? '' );
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if ( empty( $vendor_id ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $name ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '供应商名称不能为空', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $api_url ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( 'API 地址不能为空', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 URL 协议
|
||||||
|
$scheme = wp_parse_url( $api_url, PHP_URL_SCHEME );
|
||||||
|
if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( 'API 地址必须使用 http 或 https 协议', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证供应商类型
|
||||||
|
$allowed_types = [ 'woocommerce' ];
|
||||||
|
if ( ! in_array( $type, $allowed_types, true ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '不支持的供应商类型', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->get_bridge_manager()->add_vendor(
|
||||||
|
$vendor_id,
|
||||||
|
$name,
|
||||||
|
$type,
|
||||||
|
$api_url,
|
||||||
|
$consumer_key,
|
||||||
|
$consumer_secret
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $result['success'] ) {
|
||||||
|
wp_send_json_success( $result );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( $result );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 移除供应商
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_remove_vendor(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
|
||||||
|
|
||||||
|
if ( empty( $vendor_id ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->get_bridge_manager()->remove_vendor( $vendor_id );
|
||||||
|
|
||||||
|
if ( $result['success'] ) {
|
||||||
|
wp_send_json_success( $result );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( $result );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 测试供应商连接
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_test_vendor(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
|
||||||
|
|
||||||
|
if ( empty( $vendor_id ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->get_bridge_manager()->test_vendor_connection( $vendor_id );
|
||||||
|
|
||||||
|
if ( $result['success'] ) {
|
||||||
|
wp_send_json_success( $result );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( $result );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 切换供应商状态
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_toggle_vendor(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
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( [ 'message' => __( '供应商 ID 不能为空', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendors = $this->settings->get( 'vendors', [] );
|
||||||
|
|
||||||
|
if ( ! isset( $vendors[ $vendor_id ] ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '供应商不存在', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendors[ $vendor_id ]['enabled'] = $enabled;
|
||||||
|
$this->settings->set( 'vendors', $vendors );
|
||||||
|
|
||||||
|
Logger::info( 'Vendor toggled', [
|
||||||
|
'vendor_id' => $vendor_id,
|
||||||
|
'enabled' => $enabled,
|
||||||
|
] );
|
||||||
|
|
||||||
|
wp_send_json_success( [
|
||||||
|
'message' => $enabled
|
||||||
|
? __( '供应商已启用', 'wpbridge' )
|
||||||
|
: __( '供应商已禁用', 'wpbridge' ),
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 同步供应商插件列表
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_sync_vendor_plugins(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendor_id = sanitize_key( $_POST['vendor_id'] ?? '' );
|
||||||
|
|
||||||
|
$vendor_manager = $this->get_bridge_manager()->get_vendor_manager();
|
||||||
|
|
||||||
|
if ( ! empty( $vendor_id ) ) {
|
||||||
|
// 同步单个供应商
|
||||||
|
$vendor = $vendor_manager->get( $vendor_id );
|
||||||
|
if ( ! $vendor ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '供应商不存在', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugins = $vendor->get_plugins( true ); // 强制刷新
|
||||||
|
wp_send_json_success( [
|
||||||
|
'message' => sprintf(
|
||||||
|
/* translators: %d: plugin count */
|
||||||
|
__( '已同步 %d 个插件', 'wpbridge' ),
|
||||||
|
count( $plugins )
|
||||||
|
),
|
||||||
|
'count' => count( $plugins ),
|
||||||
|
] );
|
||||||
|
} else {
|
||||||
|
// 同步所有供应商
|
||||||
|
$all_plugins = $vendor_manager->get_all_plugins( true );
|
||||||
|
wp_send_json_success( [
|
||||||
|
'message' => sprintf(
|
||||||
|
/* translators: %d: plugin count */
|
||||||
|
__( '已同步 %d 个插件', 'wpbridge' ),
|
||||||
|
count( $all_plugins )
|
||||||
|
),
|
||||||
|
'count' => count( $all_plugins ),
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 添加自定义插件
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_add_custom_plugin(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_slug = sanitize_key( $_POST['plugin_slug'] ?? '' );
|
||||||
|
$name = sanitize_text_field( $_POST['name'] ?? '' );
|
||||||
|
$update_url = esc_url_raw( $_POST['update_url'] ?? '' );
|
||||||
|
|
||||||
|
if ( empty( $plugin_slug ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '插件 slug 不能为空', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = [
|
||||||
|
'name' => $name ?: $plugin_slug,
|
||||||
|
'update_url' => $update_url,
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->get_bridge_manager()->add_custom_plugin( $plugin_slug, $info );
|
||||||
|
|
||||||
|
if ( $result['success'] ) {
|
||||||
|
wp_send_json_success( $result );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( $result );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 移除自定义插件
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_remove_custom_plugin(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_slug = sanitize_key( $_POST['plugin_slug'] ?? '' );
|
||||||
|
|
||||||
|
if ( empty( $plugin_slug ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '插件 slug 不能为空', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->get_bridge_manager()->remove_custom_plugin( $plugin_slug );
|
||||||
|
|
||||||
|
if ( $result['success'] ) {
|
||||||
|
wp_send_json_success( $result );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( $result );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 测试 Bridge Server 连接
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function ajax_test_bridge_server(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( '权限不足', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$bridge_client = $this->get_bridge_manager()->get_bridge_client();
|
||||||
|
|
||||||
|
if ( ! $bridge_client ) {
|
||||||
|
wp_send_json_error( [ 'message' => __( 'Bridge Server 未配置', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $bridge_client->health_check() ) {
|
||||||
|
wp_send_json_success( [ 'message' => __( '连接成功', 'wpbridge' ) ] );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( [ 'message' => __( '连接失败', 'wpbridge' ) ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染供应商设置页面
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render_vendor_settings(): void {
|
||||||
|
$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';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商数据(用于模板)
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_vendor_data(): 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' ),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
622
includes/CLI/BridgeCommand.php
Normal file
622
includes/CLI/BridgeCommand.php
Normal file
|
|
@ -0,0 +1,622 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WP-CLI 命令
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\CLI;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Plugin;
|
||||||
|
use WPBridge\UpdateSource\SourceManager;
|
||||||
|
use WPBridge\UpdateSource\SourceModel;
|
||||||
|
use WPBridge\UpdateSource\SourceType;
|
||||||
|
use WPBridge\Cache\HealthChecker;
|
||||||
|
use WPBridge\Performance\BackgroundUpdater;
|
||||||
|
use WP_CLI;
|
||||||
|
use WP_CLI\Utils;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理 WPBridge 更新源和缓存
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* # 列出所有更新源
|
||||||
|
* $ wp bridge source list
|
||||||
|
*
|
||||||
|
* # 添加更新源
|
||||||
|
* $ wp bridge source add https://example.com/updates.json --name="My Source"
|
||||||
|
*
|
||||||
|
* # 检查所有源
|
||||||
|
* $ wp bridge check
|
||||||
|
*
|
||||||
|
* # 清除缓存
|
||||||
|
* $ wp bridge cache clear
|
||||||
|
*/
|
||||||
|
class BridgeCommand {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源管理器
|
||||||
|
*
|
||||||
|
* @var SourceManager
|
||||||
|
*/
|
||||||
|
private SourceManager $source_manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->settings = new Settings();
|
||||||
|
$this->source_manager = new SourceManager( $this->settings );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出所有更新源
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* [--format=<format>]
|
||||||
|
* : 输出格式
|
||||||
|
* ---
|
||||||
|
* default: table
|
||||||
|
* options:
|
||||||
|
* - table
|
||||||
|
* - json
|
||||||
|
* - csv
|
||||||
|
* - yaml
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [--enabled]
|
||||||
|
* : 只显示启用的源
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge source list
|
||||||
|
* $ wp bridge source list --format=json
|
||||||
|
* $ wp bridge source list --enabled
|
||||||
|
*
|
||||||
|
* @subcommand source list
|
||||||
|
*/
|
||||||
|
public function source_list( $args, $assoc_args ) {
|
||||||
|
$enabled_only = Utils\get_flag_value( $assoc_args, 'enabled', false );
|
||||||
|
|
||||||
|
$sources = $enabled_only
|
||||||
|
? $this->source_manager->get_enabled()
|
||||||
|
: $this->source_manager->get_all();
|
||||||
|
|
||||||
|
if ( empty( $sources ) ) {
|
||||||
|
WP_CLI::warning( '没有更新源' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
foreach ( $sources as $source ) {
|
||||||
|
$items[] = [
|
||||||
|
'id' => $source->id,
|
||||||
|
'name' => $source->name,
|
||||||
|
'type' => $source->type,
|
||||||
|
'api_url' => $source->api_url,
|
||||||
|
'slug' => $source->slug ?: '(all)',
|
||||||
|
'enabled' => $source->enabled ? 'yes' : 'no',
|
||||||
|
'priority' => $source->priority,
|
||||||
|
'preset' => $source->is_preset ? 'yes' : 'no',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$format = Utils\get_flag_value( $assoc_args, 'format', 'table' );
|
||||||
|
|
||||||
|
Utils\format_items( $format, $items, [
|
||||||
|
'id', 'name', 'type', 'api_url', 'slug', 'enabled', 'priority', 'preset'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加更新源
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* <url>
|
||||||
|
* : 更新源 URL
|
||||||
|
*
|
||||||
|
* [--name=<name>]
|
||||||
|
* : 源名称
|
||||||
|
*
|
||||||
|
* [--type=<type>]
|
||||||
|
* : 源类型
|
||||||
|
* ---
|
||||||
|
* default: json
|
||||||
|
* options:
|
||||||
|
* - json
|
||||||
|
* - github
|
||||||
|
* - gitlab
|
||||||
|
* - gitee
|
||||||
|
* - arkpress
|
||||||
|
* - aspirecloud
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [--slug=<slug>]
|
||||||
|
* : 插件/主题 slug
|
||||||
|
*
|
||||||
|
* [--item-type=<item_type>]
|
||||||
|
* : 项目类型
|
||||||
|
* ---
|
||||||
|
* default: plugin
|
||||||
|
* options:
|
||||||
|
* - plugin
|
||||||
|
* - theme
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [--priority=<priority>]
|
||||||
|
* : 优先级 (0-100)
|
||||||
|
* ---
|
||||||
|
* default: 50
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge source add https://example.com/updates.json --name="My Plugin"
|
||||||
|
* $ wp bridge source add github.com/user/repo --type=github --name="GitHub Plugin"
|
||||||
|
*
|
||||||
|
* @subcommand source add
|
||||||
|
*/
|
||||||
|
public function source_add( $args, $assoc_args ) {
|
||||||
|
$url = $args[0];
|
||||||
|
|
||||||
|
$source = new SourceModel();
|
||||||
|
$source->api_url = $url;
|
||||||
|
$source->name = Utils\get_flag_value( $assoc_args, 'name', '' );
|
||||||
|
$source->type = Utils\get_flag_value( $assoc_args, 'type', SourceType::JSON );
|
||||||
|
$source->slug = Utils\get_flag_value( $assoc_args, 'slug', '' );
|
||||||
|
$source->item_type = Utils\get_flag_value( $assoc_args, 'item-type', 'plugin' );
|
||||||
|
$source->priority = (int) Utils\get_flag_value( $assoc_args, 'priority', 50 );
|
||||||
|
$source->enabled = true;
|
||||||
|
|
||||||
|
// 自动检测类型
|
||||||
|
if ( empty( $assoc_args['type'] ) ) {
|
||||||
|
$source->type = $this->detect_source_type( $url );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动生成名称
|
||||||
|
if ( empty( $source->name ) ) {
|
||||||
|
$source->name = $this->generate_source_name( $url, $source->type );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证
|
||||||
|
$errors = $source->validate();
|
||||||
|
if ( ! empty( $errors ) ) {
|
||||||
|
WP_CLI::error( implode( "\n", $errors ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $this->source_manager->add( $source ) ) {
|
||||||
|
WP_CLI::success( sprintf( '已添加更新源: %s', $source->name ) );
|
||||||
|
} else {
|
||||||
|
WP_CLI::error( '添加失败' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除更新源
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* <id>
|
||||||
|
* : 源 ID
|
||||||
|
*
|
||||||
|
* [--yes]
|
||||||
|
* : 跳过确认
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge source remove source_abc123
|
||||||
|
*
|
||||||
|
* @subcommand source remove
|
||||||
|
*/
|
||||||
|
public function source_remove( $args, $assoc_args ) {
|
||||||
|
$source_id = $args[0];
|
||||||
|
|
||||||
|
$source = $this->source_manager->get( $source_id );
|
||||||
|
|
||||||
|
if ( null === $source ) {
|
||||||
|
WP_CLI::error( '源不存在' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $source->is_preset ) {
|
||||||
|
WP_CLI::error( '不能删除预置源' );
|
||||||
|
}
|
||||||
|
|
||||||
|
WP_CLI::confirm( sprintf( '确定要删除 "%s" 吗?', $source->name ), $assoc_args );
|
||||||
|
|
||||||
|
if ( $this->source_manager->delete( $source_id ) ) {
|
||||||
|
WP_CLI::success( '已删除' );
|
||||||
|
} else {
|
||||||
|
WP_CLI::error( '删除失败' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用更新源
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* <id>
|
||||||
|
* : 源 ID
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge source enable source_abc123
|
||||||
|
*
|
||||||
|
* @subcommand source enable
|
||||||
|
*/
|
||||||
|
public function source_enable( $args, $assoc_args ) {
|
||||||
|
$source_id = $args[0];
|
||||||
|
|
||||||
|
if ( $this->source_manager->toggle( $source_id, true ) ) {
|
||||||
|
WP_CLI::success( '已启用' );
|
||||||
|
} else {
|
||||||
|
WP_CLI::error( '操作失败' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用更新源
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* <id>
|
||||||
|
* : 源 ID
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge source disable source_abc123
|
||||||
|
*
|
||||||
|
* @subcommand source disable
|
||||||
|
*/
|
||||||
|
public function source_disable( $args, $assoc_args ) {
|
||||||
|
$source_id = $args[0];
|
||||||
|
|
||||||
|
if ( $this->source_manager->toggle( $source_id, false ) ) {
|
||||||
|
WP_CLI::success( '已禁用' );
|
||||||
|
} else {
|
||||||
|
WP_CLI::error( '操作失败' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查所有更新源
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* [--format=<format>]
|
||||||
|
* : 输出格式
|
||||||
|
* ---
|
||||||
|
* default: table
|
||||||
|
* options:
|
||||||
|
* - table
|
||||||
|
* - json
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge check
|
||||||
|
*
|
||||||
|
* @subcommand check
|
||||||
|
*/
|
||||||
|
public function check( $args, $assoc_args ) {
|
||||||
|
$sources = $this->source_manager->get_enabled_sorted();
|
||||||
|
|
||||||
|
if ( empty( $sources ) ) {
|
||||||
|
WP_CLI::warning( '没有启用的更新源' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$checker = new HealthChecker();
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ( $sources as $source ) {
|
||||||
|
WP_CLI::log( sprintf( '检查 %s...', $source->name ) );
|
||||||
|
|
||||||
|
$status = $checker->check( $source, true );
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'id' => $source->id,
|
||||||
|
'name' => $source->name,
|
||||||
|
'status' => $status->status,
|
||||||
|
'response_time' => $status->response_time . 'ms',
|
||||||
|
'error' => $status->error ?: '-',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$format = Utils\get_flag_value( $assoc_args, 'format', 'table' );
|
||||||
|
|
||||||
|
WP_CLI::log( '' );
|
||||||
|
Utils\format_items( $format, $results, [
|
||||||
|
'id', 'name', 'status', 'response_time', 'error'
|
||||||
|
] );
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
$healthy = count( array_filter( $results, fn( $r ) => $r['status'] === 'healthy' ) );
|
||||||
|
$total = count( $results );
|
||||||
|
|
||||||
|
WP_CLI::log( '' );
|
||||||
|
WP_CLI::log( sprintf( '健康: %d/%d', $healthy, $total ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge cache clear
|
||||||
|
*
|
||||||
|
* @subcommand cache clear
|
||||||
|
*/
|
||||||
|
public function cache_clear( $args, $assoc_args ) {
|
||||||
|
Plugin::clear_all_cache();
|
||||||
|
WP_CLI::success( '缓存已清除' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看缓存状态
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge cache status
|
||||||
|
*
|
||||||
|
* @subcommand cache status
|
||||||
|
*/
|
||||||
|
public function cache_status( $args, $assoc_args ) {
|
||||||
|
$cache = new \WPBridge\Cache\CacheManager();
|
||||||
|
$stats = $cache->get_stats();
|
||||||
|
|
||||||
|
WP_CLI::log( sprintf( 'Transient 缓存数: %d', $stats['transient_count'] ) );
|
||||||
|
WP_CLI::log( sprintf( '对象缓存: %s', $stats['object_cache'] ? '是' : '否' ) );
|
||||||
|
WP_CLI::log( sprintf( '对象缓存类型: %s', $stats['object_cache_type'] ) );
|
||||||
|
|
||||||
|
// 后台更新状态
|
||||||
|
$updater = new BackgroundUpdater( $this->settings );
|
||||||
|
$status = $updater->get_status();
|
||||||
|
|
||||||
|
WP_CLI::log( '' );
|
||||||
|
WP_CLI::log( '后台更新:' );
|
||||||
|
WP_CLI::log( sprintf( ' 已调度: %s', $status['scheduled'] ? '是' : '否' ) );
|
||||||
|
if ( $status['next_run'] ) {
|
||||||
|
WP_CLI::log( sprintf( ' 下次运行: %s (%s)', $status['next_run'], $status['next_run_human'] ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成诊断报告
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* [--format=<format>]
|
||||||
|
* : 输出格式
|
||||||
|
* ---
|
||||||
|
* default: text
|
||||||
|
* options:
|
||||||
|
* - text
|
||||||
|
* - json
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge diagnose
|
||||||
|
*
|
||||||
|
* @subcommand diagnose
|
||||||
|
*/
|
||||||
|
public function diagnose( $args, $assoc_args ) {
|
||||||
|
$format = Utils\get_flag_value( $assoc_args, 'format', 'text' );
|
||||||
|
|
||||||
|
$report = [
|
||||||
|
'wpbridge_version' => WPBRIDGE_VERSION,
|
||||||
|
'wordpress_version' => get_bloginfo( 'version' ),
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'sources' => $this->source_manager->get_stats(),
|
||||||
|
'settings' => $this->settings->get_all(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ( $format === 'json' ) {
|
||||||
|
WP_CLI::log( wp_json_encode( $report, JSON_PRETTY_PRINT ) );
|
||||||
|
} else {
|
||||||
|
WP_CLI::log( '=== WPBridge 诊断报告 ===' );
|
||||||
|
WP_CLI::log( '' );
|
||||||
|
WP_CLI::log( sprintf( 'WPBridge 版本: %s', $report['wpbridge_version'] ) );
|
||||||
|
WP_CLI::log( sprintf( 'WordPress 版本: %s', $report['wordpress_version'] ) );
|
||||||
|
WP_CLI::log( sprintf( 'PHP 版本: %s', $report['php_version'] ) );
|
||||||
|
WP_CLI::log( '' );
|
||||||
|
WP_CLI::log( '更新源统计:' );
|
||||||
|
WP_CLI::log( sprintf( ' 总数: %d', $report['sources']['total'] ) );
|
||||||
|
WP_CLI::log( sprintf( ' 已启用: %d', $report['sources']['enabled'] ) );
|
||||||
|
WP_CLI::log( '' );
|
||||||
|
WP_CLI::log( '设置:' );
|
||||||
|
WP_CLI::log( sprintf( ' 调试模式: %s', $report['settings']['debug_mode'] ? '是' : '否' ) );
|
||||||
|
WP_CLI::log( sprintf( ' 缓存时间: %d 秒', $report['settings']['cache_ttl'] ) );
|
||||||
|
WP_CLI::log( sprintf( ' 请求超时: %d 秒', $report['settings']['request_timeout'] ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出配置
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* [<file>]
|
||||||
|
* : 导出文件路径
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge config export
|
||||||
|
* $ wp bridge config export /path/to/config.json
|
||||||
|
*
|
||||||
|
* @subcommand config export
|
||||||
|
*/
|
||||||
|
public function config_export( $args, $assoc_args ) {
|
||||||
|
$sources = $this->source_manager->get_all();
|
||||||
|
$settings = $this->settings->get_all();
|
||||||
|
|
||||||
|
$export = [
|
||||||
|
'version' => WPBRIDGE_VERSION,
|
||||||
|
'exported' => current_time( 'mysql' ),
|
||||||
|
'sources' => array_map( fn( $s ) => $s->to_array(), $sources ),
|
||||||
|
'settings' => $settings,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 移除敏感信息
|
||||||
|
foreach ( $export['sources'] as &$source ) {
|
||||||
|
$source['auth_token'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = wp_json_encode( $export, JSON_PRETTY_PRINT );
|
||||||
|
|
||||||
|
if ( ! empty( $args[0] ) ) {
|
||||||
|
$file_path = $args[0];
|
||||||
|
// 验证路径是否可写
|
||||||
|
$dir = dirname( $file_path );
|
||||||
|
if ( ! is_dir( $dir ) || ! is_writable( $dir ) ) {
|
||||||
|
WP_CLI::error( '目标目录不存在或不可写' );
|
||||||
|
}
|
||||||
|
// 使用 WordPress 文件系统 API
|
||||||
|
global $wp_filesystem;
|
||||||
|
if ( ! function_exists( 'WP_Filesystem' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||||
|
}
|
||||||
|
WP_Filesystem();
|
||||||
|
if ( $wp_filesystem->put_contents( $file_path, $json, FS_CHMOD_FILE ) ) {
|
||||||
|
WP_CLI::success( sprintf( '已导出到 %s', $file_path ) );
|
||||||
|
} else {
|
||||||
|
WP_CLI::error( '写入文件失败' );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
WP_CLI::log( $json );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入配置
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* <file>
|
||||||
|
* : 配置文件路径
|
||||||
|
*
|
||||||
|
* [--yes]
|
||||||
|
* : 跳过确认
|
||||||
|
*
|
||||||
|
* ## EXAMPLES
|
||||||
|
*
|
||||||
|
* $ wp bridge config import /path/to/config.json
|
||||||
|
*
|
||||||
|
* @subcommand config import
|
||||||
|
*/
|
||||||
|
public function config_import( $args, $assoc_args ) {
|
||||||
|
$file = $args[0];
|
||||||
|
|
||||||
|
if ( ! file_exists( $file ) ) {
|
||||||
|
WP_CLI::error( '文件不存在' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 WordPress 文件系统 API
|
||||||
|
global $wp_filesystem;
|
||||||
|
if ( ! function_exists( 'WP_Filesystem' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||||
|
}
|
||||||
|
WP_Filesystem();
|
||||||
|
$json = $wp_filesystem->get_contents( $file );
|
||||||
|
|
||||||
|
if ( false === $json ) {
|
||||||
|
WP_CLI::error( '读取文件失败' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode( $json, true );
|
||||||
|
|
||||||
|
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||||
|
WP_CLI::error( 'JSON 解析失败: ' . json_last_error_msg() );
|
||||||
|
}
|
||||||
|
|
||||||
|
WP_CLI::confirm( '这将覆盖现有配置,确定继续吗?', $assoc_args );
|
||||||
|
|
||||||
|
// 导入设置
|
||||||
|
if ( ! empty( $data['settings'] ) ) {
|
||||||
|
$this->settings->update( $data['settings'] );
|
||||||
|
WP_CLI::log( '已导入设置' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入源
|
||||||
|
if ( ! empty( $data['sources'] ) ) {
|
||||||
|
$count = 0;
|
||||||
|
foreach ( $data['sources'] as $source_data ) {
|
||||||
|
// 跳过预置源
|
||||||
|
if ( ! empty( $source_data['is_preset'] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = SourceModel::from_array( $source_data );
|
||||||
|
$source->id = ''; // 生成新 ID
|
||||||
|
|
||||||
|
if ( $this->source_manager->add( $source ) ) {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WP_CLI::log( sprintf( '已导入 %d 个更新源', $count ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
WP_CLI::success( '导入完成' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测源类型
|
||||||
|
*
|
||||||
|
* @param string $url URL
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function detect_source_type( string $url ): string {
|
||||||
|
if ( strpos( $url, 'github.com' ) !== false || strpos( $url, 'github/' ) !== false ) {
|
||||||
|
return SourceType::GITHUB;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( strpos( $url, 'gitlab.com' ) !== false || strpos( $url, 'gitlab/' ) !== false ) {
|
||||||
|
return SourceType::GITLAB;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( strpos( $url, 'gitee.com' ) !== false || strpos( $url, 'gitee/' ) !== false ) {
|
||||||
|
return SourceType::GITEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SourceType::JSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成源名称
|
||||||
|
*
|
||||||
|
* @param string $url URL
|
||||||
|
* @param string $type 类型
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function generate_source_name( string $url, string $type ): string {
|
||||||
|
$parsed = parse_url( $url );
|
||||||
|
$host = $parsed['host'] ?? '';
|
||||||
|
$path = $parsed['path'] ?? '';
|
||||||
|
|
||||||
|
if ( in_array( $type, [ SourceType::GITHUB, SourceType::GITLAB, SourceType::GITEE ], true ) ) {
|
||||||
|
// 提取 owner/repo
|
||||||
|
$path = trim( $path, '/' );
|
||||||
|
$path = preg_replace( '#\.git$#', '', $path );
|
||||||
|
return $path ?: $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $host ?: $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
198
includes/Cache/CacheManager.php
Normal file
198
includes/Cache/CacheManager.php
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 缓存管理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Cache;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理器类
|
||||||
|
*/
|
||||||
|
class CacheManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存组名
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const CACHE_GROUP = 'wpbridge';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 降级到 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同时存储到 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
if ( false !== $value ) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取新值
|
||||||
|
try {
|
||||||
|
$new_value = $callback();
|
||||||
|
|
||||||
|
if ( null !== $new_value && false !== $new_value ) {
|
||||||
|
$this->set( $key, $new_value, $ttl );
|
||||||
|
|
||||||
|
// 同时存储一份过期缓存备份
|
||||||
|
$this->set( $key . '_stale', $new_value, $stale_ttl );
|
||||||
|
|
||||||
|
return $new_value;
|
||||||
|
}
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
// 获取新值失败,尝试使用过期缓存
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试使用过期缓存
|
||||||
|
$stale_value = $this->get( $key . '_stale' );
|
||||||
|
|
||||||
|
if ( false !== $stale_value ) {
|
||||||
|
return $stale_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有缓存
|
||||||
|
*/
|
||||||
|
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_' ) . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 清除对象缓存组(不使用 flush 避免影响其他插件)
|
||||||
|
if ( wp_using_ext_object_cache() ) {
|
||||||
|
wp_cache_delete( 'wpbridge', 'wpbridge' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存统计
|
||||||
|
*
|
||||||
|
* @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_' ) . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wp_object_cache;
|
||||||
|
|
||||||
|
if ( isset( $wp_object_cache->redis ) || class_exists( 'Redis' ) ) {
|
||||||
|
return 'redis';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $wp_object_cache->mc ) || class_exists( 'Memcached' ) ) {
|
||||||
|
return 'memcached';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
243
includes/Cache/FallbackStrategy.php
Normal file
243
includes/Cache/FallbackStrategy.php
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 降级策略
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Cache;
|
||||||
|
|
||||||
|
use WPBridge\UpdateSource\SourceModel;
|
||||||
|
use WPBridge\UpdateSource\SourceManager;
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 降级策略类
|
||||||
|
*/
|
||||||
|
class FallbackStrategy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源不可用时的行为
|
||||||
|
*/
|
||||||
|
const ON_FAIL_SKIP = 'skip'; // 跳过,继续下一个源
|
||||||
|
const ON_FAIL_WARN = 'warn'; // 警告,但继续
|
||||||
|
const ON_FAIL_BLOCK = 'block'; // 阻止更新检查
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理器
|
||||||
|
*
|
||||||
|
* @var CacheManager
|
||||||
|
*/
|
||||||
|
private CacheManager $cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康检查器
|
||||||
|
*
|
||||||
|
* @var HealthChecker
|
||||||
|
*/
|
||||||
|
private HealthChecker $health_checker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大重试次数
|
||||||
|
*
|
||||||
|
* @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 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( '源在冷却期,跳过', [ 'source' => $source->id ] );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缓存的健康状态
|
||||||
|
$status = $this->health_checker->get_status( $source->id );
|
||||||
|
|
||||||
|
if ( null !== $status && ! $status->is_available() ) {
|
||||||
|
Logger::debug( '源不可用,跳过', [ 'source' => $source->id ] );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$available[] = $source;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
if ( empty( $available ) ) {
|
||||||
|
Logger::warning( '没有可用的更新源' );
|
||||||
|
|
||||||
|
// 尝试使用过期缓存
|
||||||
|
if ( ! empty( $cache_key ) ) {
|
||||||
|
$stale = $this->cache->get( $cache_key . '_stale' );
|
||||||
|
if ( false !== $stale ) {
|
||||||
|
Logger::info( '使用过期缓存', [ 'key' => $cache_key ] );
|
||||||
|
return $stale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$last_error = null;
|
||||||
|
|
||||||
|
foreach ( $available as $source ) {
|
||||||
|
$retries = 0;
|
||||||
|
|
||||||
|
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 天
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回 null/false 但没有异常,不重试
|
||||||
|
break;
|
||||||
|
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$last_error = $e;
|
||||||
|
$retries++;
|
||||||
|
|
||||||
|
Logger::warning( '操作失败,重试中', [
|
||||||
|
'source' => $source->id,
|
||||||
|
'retry' => $retries,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
] );
|
||||||
|
|
||||||
|
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( '所有源失败,使用过期缓存', [ 'key' => $cache_key ] );
|
||||||
|
return $stale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( null !== $last_error ) {
|
||||||
|
Logger::error( '所有源都失败', [ 'error' => $last_error->getMessage() ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
Logger::warning( '源失败', [
|
||||||
|
'source' => $source->id,
|
||||||
|
'error' => $error,
|
||||||
|
'behavior' => $behavior,
|
||||||
|
] );
|
||||||
|
|
||||||
|
switch ( $behavior ) {
|
||||||
|
case self::ON_FAIL_WARN:
|
||||||
|
// 添加管理员通知
|
||||||
|
$this->add_admin_notice( $source, $error );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::ON_FAIL_BLOCK:
|
||||||
|
// 阻止更新检查(不推荐)
|
||||||
|
throw new \RuntimeException( sprintf(
|
||||||
|
__( '更新源 %s 不可用: %s', 'wpbridge' ),
|
||||||
|
$source->name,
|
||||||
|
$error
|
||||||
|
) );
|
||||||
|
|
||||||
|
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', [] );
|
||||||
|
|
||||||
|
$notices[] = [
|
||||||
|
'type' => 'warning',
|
||||||
|
'message' => sprintf(
|
||||||
|
__( '更新源 "%s" 暂时不可用: %s', 'wpbridge' ),
|
||||||
|
$source->name,
|
||||||
|
$error
|
||||||
|
),
|
||||||
|
'time' => time(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 只保留最近 10 条通知
|
||||||
|
$notices = array_slice( $notices, -10 );
|
||||||
|
|
||||||
|
update_option( 'wpbridge_admin_notices', $notices );
|
||||||
|
}
|
||||||
|
}
|
||||||
187
includes/Cache/HealthChecker.php
Normal file
187
includes/Cache/HealthChecker.php
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 健康检查器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Cache;
|
||||||
|
|
||||||
|
use WPBridge\UpdateSource\SourceModel;
|
||||||
|
use WPBridge\UpdateSource\Handlers\HealthStatus;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康检查器类
|
||||||
|
*/
|
||||||
|
class HealthChecker {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理器
|
||||||
|
*
|
||||||
|
* @var CacheManager
|
||||||
|
*/
|
||||||
|
private CacheManager $cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康状态缓存 TTL(1 小时)
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const HEALTH_CACHE_TTL = 3600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败源冷却时间(30 分钟)
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const FAILED_COOLDOWN = 1800;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
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' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行健康检查
|
||||||
|
$handler = $source->get_handler();
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
// 如果失败,设置冷却
|
||||||
|
if ( ! $status->is_available() ) {
|
||||||
|
$this->set_cooldown( $source->id );
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::debug( '健康检查完成', [
|
||||||
|
'source' => $source->id,
|
||||||
|
'status' => $status->status,
|
||||||
|
'time' => $status->response_time,
|
||||||
|
] );
|
||||||
|
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量检查源健康状态
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
if ( false !== $cached && $cached instanceof HealthStatus ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
private function set_cooldown( string $source_id ): void {
|
||||||
|
$this->cache->set( 'cooldown_' . $source_id, time(), 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有健康状态缓存
|
||||||
|
*/
|
||||||
|
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_' ) . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
370
includes/Commercial/BridgeClient.php
Normal file
370
includes/Commercial/BridgeClient.php
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bridge Server 客户端
|
||||||
|
*
|
||||||
|
* 与 wpbridge-server Go 服务端通信
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.8
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WPBridge\Commercial;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BridgeClient 类
|
||||||
|
*/
|
||||||
|
class BridgeClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务端 URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $server_url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Key
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private string $api_key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求超时(秒)
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private int $timeout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param string $server_url 服务端 URL
|
||||||
|
* @param string $api_key API Key
|
||||||
|
* @param int $timeout 请求超时(秒)
|
||||||
|
*/
|
||||||
|
public function __construct( string $server_url, string $api_key, int $timeout = 30 ) {
|
||||||
|
$this->server_url = rtrim( $server_url, '/' );
|
||||||
|
$this->api_key = $api_key;
|
||||||
|
$this->timeout = $timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件信息
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function get_plugin_info( string $slug ): ?array {
|
||||||
|
$response = $this->request( 'GET', "/api/v1/plugin/{$slug}" );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
Logger::error( 'Failed to get plugin info', [
|
||||||
|
'slug' => $slug,
|
||||||
|
'error' => $response->get_error_message(),
|
||||||
|
] );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载 URL
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function get_download_url( string $slug ): ?string {
|
||||||
|
// 下载端点会返回重定向或直接代理
|
||||||
|
return $this->server_url . "/api/v1/download/{$slug}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出所有供应商(需要认证)
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function list_vendors(): array {
|
||||||
|
$response = $this->request( 'GET', '/api/v1/admin/vendors', [], true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
Logger::error( 'Failed to list vendors', [
|
||||||
|
'error' => $response->get_error_message(),
|
||||||
|
] );
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建供应商(需要认证)
|
||||||
|
*
|
||||||
|
* @param array $data 供应商数据
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function create_vendor( array $data ): array {
|
||||||
|
$response = $this->request( 'POST', '/api/v1/admin/vendors', $data, true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $response->get_error_message(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $response,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新供应商(需要认证)
|
||||||
|
*
|
||||||
|
* @param int $id 供应商 ID
|
||||||
|
* @param array $data 供应商数据
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function update_vendor( int $id, array $data ): array {
|
||||||
|
$response = $this->request( 'PUT', "/api/v1/admin/vendors/{$id}", $data, true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $response->get_error_message(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $response,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除供应商(需要认证)
|
||||||
|
*
|
||||||
|
* @param int $id 供应商 ID
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function delete_vendor( int $id ): array {
|
||||||
|
$response = $this->request( 'DELETE', "/api/v1/admin/vendors/{$id}", [], true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $response->get_error_message(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出所有插件(需要认证)
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function list_plugins(): array {
|
||||||
|
$response = $this->request( 'GET', '/api/v1/admin/plugins', [], true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
Logger::error( 'Failed to list plugins', [
|
||||||
|
'error' => $response->get_error_message(),
|
||||||
|
] );
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建插件(需要认证)
|
||||||
|
*
|
||||||
|
* @param array $data 插件数据
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function create_plugin( array $data ): array {
|
||||||
|
$response = $this->request( 'POST', '/api/v1/admin/plugins', $data, true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $response->get_error_message(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $response,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件详情(需要认证)
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function get_plugin( string $slug ): ?array {
|
||||||
|
$response = $this->request( 'GET', "/api/v1/admin/plugins/{$slug}", [], true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新插件(需要认证)
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @param array $data 插件数据
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function update_plugin( string $slug, array $data ): array {
|
||||||
|
$response = $this->request( 'PUT', "/api/v1/admin/plugins/{$slug}", $data, true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $response->get_error_message(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $response,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除插件(需要认证)
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function delete_plugin( string $slug ): array {
|
||||||
|
$response = $this->request( 'DELETE', "/api/v1/admin/plugins/{$slug}", [], true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $response->get_error_message(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康检查
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function health_check(): bool {
|
||||||
|
$response = $this->request( 'GET', '/health' );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset( $response['status'] ) && $response['status'] === 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送请求
|
||||||
|
*
|
||||||
|
* @param string $method HTTP 方法
|
||||||
|
* @param string $endpoint API 端点
|
||||||
|
* @param array $data 请求数据
|
||||||
|
* @param bool $auth 是否需要认证
|
||||||
|
* @return array|\WP_Error
|
||||||
|
*/
|
||||||
|
private function request( string $method, string $endpoint, array $data = [], bool $auth = false ) {
|
||||||
|
$url = $this->server_url . $endpoint;
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
'method' => $method,
|
||||||
|
'timeout' => $this->timeout,
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 添加认证头
|
||||||
|
if ( $auth && ! empty( $this->api_key ) ) {
|
||||||
|
$args['headers']['X-API-Key'] = $this->api_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加请求体
|
||||||
|
if ( ! empty( $data ) && in_array( $method, [ 'POST', 'PUT', 'PATCH' ], true ) ) {
|
||||||
|
$args['body'] = wp_json_encode( $data );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_request( $url, $args );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status_code = wp_remote_retrieve_response_code( $response );
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
|
||||||
|
// 处理 204 No Content
|
||||||
|
if ( $status_code === 204 ) {
|
||||||
|
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, [ 'status' => $status_code ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 JSON 响应
|
||||||
|
$decoded = json_decode( $body, true );
|
||||||
|
|
||||||
|
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||||
|
return new \WP_Error( 'json_decode_error', 'Invalid JSON response' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务端 URL
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_server_url(): string {
|
||||||
|
return $this->server_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已配置
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function is_configured(): bool {
|
||||||
|
return ! empty( $this->server_url ) && ! empty( $this->api_key );
|
||||||
|
}
|
||||||
|
}
|
||||||
672
includes/Commercial/BridgeManager.php
Normal file
672
includes/Commercial/BridgeManager.php
Normal file
|
|
@ -0,0 +1,672 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 商业插件桥接管理器
|
||||||
|
*
|
||||||
|
* 管理桥接插件列表,提供启用/禁用功能
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.7
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WPBridge\Commercial;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\RemoteConfig;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Commercial\Vendors\VendorManager;
|
||||||
|
use WPBridge\Security\Encryption;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BridgeManager 类
|
||||||
|
*/
|
||||||
|
class BridgeManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程配置实例
|
||||||
|
*
|
||||||
|
* @var RemoteConfig
|
||||||
|
*/
|
||||||
|
private RemoteConfig $remote_config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPL 验证器
|
||||||
|
*
|
||||||
|
* @var GPLValidator
|
||||||
|
*/
|
||||||
|
private GPLValidator $gpl_validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商管理器
|
||||||
|
*
|
||||||
|
* @var VendorManager
|
||||||
|
*/
|
||||||
|
private VendorManager $vendor_manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridge Server 客户端
|
||||||
|
*
|
||||||
|
* @var BridgeClient|null
|
||||||
|
*/
|
||||||
|
private ?BridgeClient $bridge_client = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
* @param RemoteConfig $remote_config 远程配置实例
|
||||||
|
*/
|
||||||
|
public function __construct( Settings $settings, RemoteConfig $remote_config ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->remote_config = $remote_config;
|
||||||
|
$this->gpl_validator = new GPLValidator();
|
||||||
|
$this->vendor_manager = new VendorManager();
|
||||||
|
|
||||||
|
// 初始化 Bridge Server 客户端
|
||||||
|
$this->init_bridge_client();
|
||||||
|
|
||||||
|
// 初始化已配置的供应商
|
||||||
|
$this->init_vendors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 Bridge Server 客户端
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function init_bridge_client(): void {
|
||||||
|
$server_url = $this->settings->get( 'bridge_server_url', '' );
|
||||||
|
// API Key 使用加密存储
|
||||||
|
$api_key = Encryption::get_secure( 'bridge_server_api_key', '' );
|
||||||
|
|
||||||
|
if ( ! empty( $server_url ) ) {
|
||||||
|
$this->bridge_client = new BridgeClient( $server_url, $api_key );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Bridge Server 客户端
|
||||||
|
*
|
||||||
|
* @return BridgeClient|null
|
||||||
|
*/
|
||||||
|
public function get_bridge_client(): ?BridgeClient {
|
||||||
|
return $this->bridge_client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Bridge Server 配置
|
||||||
|
*
|
||||||
|
* @param string $server_url 服务端 URL
|
||||||
|
* @param string $api_key API Key
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function set_bridge_server( string $server_url, string $api_key ): array {
|
||||||
|
// 验证连接
|
||||||
|
$client = new BridgeClient( $server_url, $api_key );
|
||||||
|
|
||||||
|
if ( ! $client->health_check() ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => __( '无法连接到 Bridge Server', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置(URL 明文存储,API Key 加密存储)
|
||||||
|
$this->settings->set( 'bridge_server_url', $server_url );
|
||||||
|
Encryption::store_secure( 'bridge_server_api_key', $api_key );
|
||||||
|
|
||||||
|
$this->bridge_client = $client;
|
||||||
|
|
||||||
|
Logger::info( 'Bridge server configured', [ 'url' => $server_url ] );
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => __( 'Bridge Server 配置成功', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化供应商
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function init_vendors(): void {
|
||||||
|
$vendor_configs = $this->settings->get( 'vendors', [] );
|
||||||
|
|
||||||
|
foreach ( $vendor_configs as $vendor_id => $config ) {
|
||||||
|
if ( empty( $config['enabled'] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $config['type'] ?? 'woocommerce';
|
||||||
|
|
||||||
|
switch ( $type ) {
|
||||||
|
case 'woocommerce':
|
||||||
|
$vendor = new Vendors\WooCommerceVendor(
|
||||||
|
$vendor_id,
|
||||||
|
$config['name'] ?? $vendor_id,
|
||||||
|
$config['api_url'] ?? '',
|
||||||
|
$config['consumer_key'] ?? '',
|
||||||
|
$config['consumer_secret'] ?? ''
|
||||||
|
);
|
||||||
|
$this->vendor_manager->register( $vendor );
|
||||||
|
break;
|
||||||
|
// 未来可扩展其他供应商类型
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可桥接的商业插件列表(从服务端)
|
||||||
|
*
|
||||||
|
* 优先从 Bridge Server 获取,回退到 RemoteConfig
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_available_plugins(): array {
|
||||||
|
// 优先使用 Bridge Server
|
||||||
|
if ( $this->bridge_client && $this->bridge_client->is_configured() ) {
|
||||||
|
$plugins = $this->bridge_client->list_plugins();
|
||||||
|
if ( ! empty( $plugins ) ) {
|
||||||
|
// 转换为 slug => info 格式
|
||||||
|
$result = [];
|
||||||
|
foreach ( $plugins as $plugin ) {
|
||||||
|
$result[ $plugin['slug'] ] = $plugin;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退到 RemoteConfig
|
||||||
|
return $this->remote_config->get( 'bridgeable_plugins', [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件下载 URL
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function get_plugin_download_url( string $slug ): ?string {
|
||||||
|
if ( $this->bridge_client && $this->bridge_client->is_configured() ) {
|
||||||
|
return $this->bridge_client->get_download_url( $slug );
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有可用插件(混合模式)
|
||||||
|
*
|
||||||
|
* 合并三个来源:
|
||||||
|
* 1. 官方优化列表(服务端)
|
||||||
|
* 2. 供应商渠道插件
|
||||||
|
* 3. 用户自定义插件
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_all_available_plugins(): array {
|
||||||
|
$plugins = [];
|
||||||
|
|
||||||
|
// 1. 官方优化列表
|
||||||
|
$official = $this->get_available_plugins();
|
||||||
|
foreach ( $official as $slug => $info ) {
|
||||||
|
$plugins[ $slug ] = array_merge( $info, [
|
||||||
|
'source' => 'official',
|
||||||
|
'vendor' => null,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 供应商渠道插件
|
||||||
|
$vendor_plugins = $this->vendor_manager->get_all_plugins();
|
||||||
|
foreach ( $vendor_plugins as $slug => $info ) {
|
||||||
|
if ( ! isset( $plugins[ $slug ] ) ) {
|
||||||
|
$plugins[ $slug ] = $info;
|
||||||
|
} else {
|
||||||
|
// 官方列表优先,但记录供应商也有
|
||||||
|
$plugins[ $slug ]['also_available_from'] = $info['vendor'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 用户自定义插件
|
||||||
|
$custom = $this->settings->get( 'custom_plugins', [] );
|
||||||
|
foreach ( $custom as $slug => $info ) {
|
||||||
|
if ( ! isset( $plugins[ $slug ] ) ) {
|
||||||
|
$plugins[ $slug ] = array_merge( $info, [
|
||||||
|
'source' => 'custom',
|
||||||
|
'vendor' => null,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商管理器
|
||||||
|
*
|
||||||
|
* @return VendorManager
|
||||||
|
*/
|
||||||
|
public function get_vendor_manager(): VendorManager {
|
||||||
|
return $this->vendor_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已启用桥接的插件
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_bridged_plugins(): array {
|
||||||
|
return $this->settings->get( 'bridged_plugins', [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用插件桥接
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param string $plugin_file 插件文件路径(可选,用于 GPL 验证)
|
||||||
|
* @return array 包含 success 和 message 的结果
|
||||||
|
*/
|
||||||
|
public function enable_bridge( string $plugin_slug, string $plugin_file = '' ): array {
|
||||||
|
// 1. 检查是否在可桥接列表(混合模式)
|
||||||
|
$all_available = $this->get_all_available_plugins();
|
||||||
|
if ( ! isset( $all_available[ $plugin_slug ] ) ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => __( '该插件不在可桥接列表中', 'wpbridge' ),
|
||||||
|
'code' => 'not_available',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_info = $all_available[ $plugin_slug ];
|
||||||
|
|
||||||
|
// 2. H5 修复: GPL 合规验证
|
||||||
|
$gpl_result = $this->gpl_validator->validate( $plugin_slug, $plugin_file );
|
||||||
|
if ( $gpl_result['is_gpl'] === false ) {
|
||||||
|
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', [
|
||||||
|
'plugin' => $plugin_slug,
|
||||||
|
'result' => $gpl_result,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查订阅限制
|
||||||
|
$limit_check = $this->check_subscription_limit();
|
||||||
|
if ( ! $limit_check['allowed'] ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $limit_check['message'],
|
||||||
|
'code' => 'limit_exceeded',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 添加到桥接列表
|
||||||
|
$bridged = $this->get_bridged_plugins();
|
||||||
|
if ( ! in_array( $plugin_slug, $bridged, true ) ) {
|
||||||
|
$bridged[] = $plugin_slug;
|
||||||
|
$this->settings->set( 'bridged_plugins', $bridged );
|
||||||
|
|
||||||
|
Logger::info( 'Plugin bridge enabled', [
|
||||||
|
'plugin' => $plugin_slug,
|
||||||
|
'gpl_result' => $gpl_result,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => __( '桥接已启用', 'wpbridge' ),
|
||||||
|
'code' => 'enabled',
|
||||||
|
'gpl_result' => $gpl_result,
|
||||||
|
'source' => $plugin_info['source'] ?? 'official',
|
||||||
|
'vendor' => $plugin_info['vendor'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用插件桥接
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function disable_bridge( string $plugin_slug ): array {
|
||||||
|
$bridged = $this->get_bridged_plugins();
|
||||||
|
$bridged = array_diff( $bridged, [ $plugin_slug ] );
|
||||||
|
$result = $this->settings->set( 'bridged_plugins', array_values( $bridged ) );
|
||||||
|
|
||||||
|
if ( $result ) {
|
||||||
|
Logger::info( 'Plugin bridge disabled', [ 'plugin' => $plugin_slug ] );
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => __( '桥接已禁用', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => __( '禁用失败', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查插件是否已桥接
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function is_bridged( string $plugin_slug ): bool {
|
||||||
|
return in_array( $plugin_slug, $this->get_bridged_plugins(), true );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查订阅限制
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function check_subscription_limit(): array {
|
||||||
|
$subscription = $this->get_subscription();
|
||||||
|
|
||||||
|
// Agency 计划无限制
|
||||||
|
if ( $subscription['plan'] === 'agency' ) {
|
||||||
|
return [ 'allowed' => true ];
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_count = count( $this->get_bridged_plugins() );
|
||||||
|
$limit = $subscription['plugins_limit'] ?? 5;
|
||||||
|
|
||||||
|
if ( $current_count >= $limit ) {
|
||||||
|
return [
|
||||||
|
'allowed' => false,
|
||||||
|
'message' => sprintf(
|
||||||
|
/* translators: %d: plugin limit */
|
||||||
|
__( '已达到插件数量限制 (%d),请升级订阅', 'wpbridge' ),
|
||||||
|
$limit
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ 'allowed' => true ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅信息
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_subscription(): array {
|
||||||
|
$default = [
|
||||||
|
'plan' => 'free',
|
||||||
|
'plugins_limit' => 0,
|
||||||
|
'site_limit' => 1,
|
||||||
|
'status' => 'active',
|
||||||
|
'expires_at' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$subscription = $this->settings->get( 'subscription', [] );
|
||||||
|
return array_merge( $default, $subscription );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取桥接状态统计
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_stats(): array {
|
||||||
|
$subscription = $this->get_subscription();
|
||||||
|
$bridged = $this->get_bridged_plugins();
|
||||||
|
$available = $this->get_available_plugins();
|
||||||
|
|
||||||
|
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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已安装的可桥接插件
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_installed_bridgeable_plugins(): array {
|
||||||
|
if ( ! function_exists( 'get_plugins' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$all_plugins = get_plugins();
|
||||||
|
$available = $this->get_available_plugins();
|
||||||
|
$bridged = $this->get_bridged_plugins();
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ( $all_plugins as $file => $data ) {
|
||||||
|
$slug = dirname( $file );
|
||||||
|
if ( $slug === '.' ) {
|
||||||
|
$slug = basename( $file, '.php' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $available[ $slug ] ) ) {
|
||||||
|
$gpl_result = $this->gpl_validator->validate( $slug, $file );
|
||||||
|
|
||||||
|
$result[ $slug ] = [
|
||||||
|
'file' => $file,
|
||||||
|
'name' => $data['Name'],
|
||||||
|
'version' => $data['Version'],
|
||||||
|
'is_bridged' => in_array( $slug, $bridged, true ),
|
||||||
|
'gpl_status' => $gpl_result,
|
||||||
|
'available' => $available[ $slug ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步可桥接插件列表
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function sync_available_plugins(): bool {
|
||||||
|
return $this->remote_config->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加供应商
|
||||||
|
*
|
||||||
|
* @param string $vendor_id 供应商 ID
|
||||||
|
* @param string $name 供应商名称
|
||||||
|
* @param string $type 供应商类型 (woocommerce)
|
||||||
|
* @param string $api_url API 地址
|
||||||
|
* @param string $consumer_key Consumer Key
|
||||||
|
* @param string $consumer_secret Consumer Secret
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function add_vendor(
|
||||||
|
string $vendor_id,
|
||||||
|
string $name,
|
||||||
|
string $type,
|
||||||
|
string $api_url,
|
||||||
|
string $consumer_key,
|
||||||
|
string $consumer_secret
|
||||||
|
): array {
|
||||||
|
$vendors = $this->settings->get( 'vendors', [] );
|
||||||
|
|
||||||
|
if ( isset( $vendors[ $vendor_id ] ) ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => __( '供应商 ID 已存在', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendors[ $vendor_id ] = [
|
||||||
|
'name' => $name,
|
||||||
|
'type' => $type,
|
||||||
|
'api_url' => $api_url,
|
||||||
|
'consumer_key' => $consumer_key,
|
||||||
|
'consumer_secret' => $consumer_secret,
|
||||||
|
'enabled' => true,
|
||||||
|
'created_at' => time(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->settings->set( 'vendors', $vendors );
|
||||||
|
|
||||||
|
// 立即注册供应商
|
||||||
|
if ( $type === 'woocommerce' ) {
|
||||||
|
$vendor = new Vendors\WooCommerceVendor(
|
||||||
|
$vendor_id,
|
||||||
|
$name,
|
||||||
|
$api_url,
|
||||||
|
$consumer_key,
|
||||||
|
$consumer_secret
|
||||||
|
);
|
||||||
|
$this->vendor_manager->register( $vendor );
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::info( 'Vendor added', [ 'vendor_id' => $vendor_id, 'type' => $type ] );
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => __( '供应商已添加', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除供应商
|
||||||
|
*
|
||||||
|
* @param string $vendor_id 供应商 ID
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function remove_vendor( string $vendor_id ): array {
|
||||||
|
$vendors = $this->settings->get( 'vendors', [] );
|
||||||
|
|
||||||
|
if ( ! isset( $vendors[ $vendor_id ] ) ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => __( '供应商不存在', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
unset( $vendors[ $vendor_id ] );
|
||||||
|
$this->settings->set( 'vendors', $vendors );
|
||||||
|
$this->vendor_manager->unregister( $vendor_id );
|
||||||
|
|
||||||
|
Logger::info( 'Vendor removed', [ 'vendor_id' => $vendor_id ] );
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => __( '供应商已移除', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有供应商
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_vendors(): array {
|
||||||
|
return $this->settings->get( 'vendors', [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试供应商连接
|
||||||
|
*
|
||||||
|
* @param string $vendor_id 供应商 ID
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function test_vendor_connection( string $vendor_id ): array {
|
||||||
|
$vendor = $this->vendor_manager->get( $vendor_id );
|
||||||
|
|
||||||
|
if ( ! $vendor ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => __( '供应商不存在或未启用', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $vendor->test_connection();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => $result,
|
||||||
|
'message' => $result
|
||||||
|
? __( '连接成功', 'wpbridge' )
|
||||||
|
: __( '连接失败', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加自定义插件
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param array $info 插件信息
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function add_custom_plugin( string $plugin_slug, array $info ): array {
|
||||||
|
$custom = $this->settings->get( 'custom_plugins', [] );
|
||||||
|
|
||||||
|
$custom[ $plugin_slug ] = array_merge( $info, [
|
||||||
|
'added_at' => time(),
|
||||||
|
] );
|
||||||
|
|
||||||
|
$this->settings->set( 'custom_plugins', $custom );
|
||||||
|
|
||||||
|
Logger::info( 'Custom plugin added', [ 'plugin' => $plugin_slug ] );
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => __( '自定义插件已添加', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除自定义插件
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function remove_custom_plugin( string $plugin_slug ): array {
|
||||||
|
$custom = $this->settings->get( 'custom_plugins', [] );
|
||||||
|
|
||||||
|
if ( ! isset( $custom[ $plugin_slug ] ) ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => __( '自定义插件不存在', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
unset( $custom[ $plugin_slug ] );
|
||||||
|
$this->settings->set( 'custom_plugins', $custom );
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => __( '自定义插件已移除', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
391
includes/Commercial/CommercialManager.php
Normal file
391
includes/Commercial/CommercialManager.php
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 商业插件管理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Commercial;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Security\Validator;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商业插件管理器类
|
||||||
|
* 处理商业插件的更新源覆盖和版本管理
|
||||||
|
*/
|
||||||
|
class CommercialManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已注册的商业插件
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $registered_plugins = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本锁定列表
|
||||||
|
*
|
||||||
|
* @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', [] );
|
||||||
|
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
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, [
|
||||||
|
'name' => $slug,
|
||||||
|
'license_type' => 'unknown',
|
||||||
|
'update_source' => '',
|
||||||
|
'backup_enabled' => true,
|
||||||
|
] );
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
$all_plugins = get_plugins();
|
||||||
|
$commercial_plugins = [];
|
||||||
|
|
||||||
|
foreach ( $all_plugins as $file => $data ) {
|
||||||
|
$slug = dirname( $file );
|
||||||
|
|
||||||
|
// 检测商业插件特征
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否是商业插件
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
|
||||||
|
if ( in_array( $slug, $known_commercial, true ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查插件头信息
|
||||||
|
$plugin_path = WP_PLUGIN_DIR . '/' . $file;
|
||||||
|
|
||||||
|
// 路径安全验证:防止路径遍历
|
||||||
|
$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 ( file_exists( $real_path ) ) {
|
||||||
|
$content = file_get_contents( $real_path, false, null, 0, 8192 );
|
||||||
|
|
||||||
|
// 检查授权相关关键词
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if ( ! file_exists( $plugin_path ) ) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents( $plugin_path, false, null, 0, 8192 );
|
||||||
|
|
||||||
|
// 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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envato
|
||||||
|
if ( stripos( $content, 'envato' ) !== false ) {
|
||||||
|
return 'envato';
|
||||||
|
}
|
||||||
|
|
||||||
|
// WPML
|
||||||
|
if ( stripos( $content, 'OTGS' ) !== false ) {
|
||||||
|
return 'otgs';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤更新
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
|
||||||
|
// 检查版本锁定
|
||||||
|
if ( $this->is_version_locked( $slug ) ) {
|
||||||
|
$locked_version = $this->get_locked_version( $slug );
|
||||||
|
|
||||||
|
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 ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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( '无权限锁定版本', [ 'slug' => $slug, 'user' => get_current_user_id() ] );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证版本号格式
|
||||||
|
if ( ! Validator::is_valid_version( $version ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 );
|
||||||
|
|
||||||
|
if ( $result ) {
|
||||||
|
Logger::info( '版本已锁定', [ 'slug' => $slug, 'version' => $version ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解锁版本
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset( $this->version_locks[ $slug ] );
|
||||||
|
|
||||||
|
$result = $this->settings->set( 'version_locks', $this->version_locks );
|
||||||
|
|
||||||
|
if ( $result ) {
|
||||||
|
Logger::info( '版本已解锁', [ 'slug' => $slug ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新完成时触发
|
||||||
|
*
|
||||||
|
* @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'] ?? [];
|
||||||
|
|
||||||
|
foreach ( $plugins as $file ) {
|
||||||
|
$slug = dirname( $file );
|
||||||
|
|
||||||
|
// 触发更新完成事件
|
||||||
|
do_action( 'wpbridge_plugin_updated', $slug, $file );
|
||||||
|
|
||||||
|
Logger::info( '插件更新完成', [ 'slug' => $slug ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已注册的商业插件
|
||||||
|
*
|
||||||
|
* @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 [
|
||||||
|
'detected_count' => count( $detected ),
|
||||||
|
'registered_count' => count( $this->registered_plugins ),
|
||||||
|
'locked_count' => count( $this->version_locks ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
359
includes/Commercial/GPLValidator.php
Normal file
359
includes/Commercial/GPLValidator.php
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* GPL 合规验证器
|
||||||
|
*
|
||||||
|
* 自动检测插件是否为 GPL 兼容授权
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.7
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WPBridge\Commercial;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPLValidator 类
|
||||||
|
*/
|
||||||
|
class GPLValidator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPL 兼容的授权标识
|
||||||
|
*/
|
||||||
|
private const GPL_COMPATIBLE_LICENSES = [
|
||||||
|
'gpl',
|
||||||
|
'gpl-2.0',
|
||||||
|
'gpl-2.0+',
|
||||||
|
'gpl-2.0-or-later',
|
||||||
|
'gpl-3.0',
|
||||||
|
'gpl-3.0+',
|
||||||
|
'gpl-3.0-or-later',
|
||||||
|
'gplv2',
|
||||||
|
'gplv3',
|
||||||
|
'gnu general public license',
|
||||||
|
'gnu gpl',
|
||||||
|
'lgpl',
|
||||||
|
'lgpl-2.1',
|
||||||
|
'lgpl-3.0',
|
||||||
|
'mit',
|
||||||
|
'apache-2.0',
|
||||||
|
'bsd',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已知的 GPL 商业插件列表
|
||||||
|
*/
|
||||||
|
private const KNOWN_GPL_PLUGINS = [
|
||||||
|
'elementor-pro',
|
||||||
|
'wordpress-seo-premium',
|
||||||
|
'advanced-custom-fields-pro',
|
||||||
|
'gravityforms',
|
||||||
|
'wpforms',
|
||||||
|
'wpforms-lite',
|
||||||
|
'ninja-forms',
|
||||||
|
'seo-by-rank-math-pro',
|
||||||
|
'wp-rocket',
|
||||||
|
'perfmatters',
|
||||||
|
'flavor',
|
||||||
|
'updraftplus',
|
||||||
|
'updraftplus-premium',
|
||||||
|
'memberpress',
|
||||||
|
'learndash',
|
||||||
|
'woocommerce-subscriptions',
|
||||||
|
'woocommerce-memberships',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已知的非 GPL 插件列表(不应桥接)
|
||||||
|
*/
|
||||||
|
private const NON_GPL_PLUGINS = [
|
||||||
|
// Envato 独占插件通常不是 GPL
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证结果缓存
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $cache = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证插件是否 GPL 兼容
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param string $plugin_file 插件文件路径(可选)
|
||||||
|
* @return array 包含 is_gpl, confidence, source 的数组
|
||||||
|
*/
|
||||||
|
public function validate( string $plugin_slug, string $plugin_file = '' ): array {
|
||||||
|
// 检查缓存
|
||||||
|
if ( isset( $this->cache[ $plugin_slug ] ) ) {
|
||||||
|
return $this->cache[ $plugin_slug ];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->do_validate( $plugin_slug, $plugin_file );
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
$this->cache[ $plugin_slug ] = $result;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行验证
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param string $plugin_file 插件文件路径
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function do_validate( string $plugin_slug, string $plugin_file ): array {
|
||||||
|
// 1. 检查已知列表
|
||||||
|
if ( in_array( $plugin_slug, self::KNOWN_GPL_PLUGINS, true ) ) {
|
||||||
|
return [
|
||||||
|
'is_gpl' => true,
|
||||||
|
'confidence' => 100,
|
||||||
|
'source' => 'known_list',
|
||||||
|
'license' => 'GPL-2.0+',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( in_array( $plugin_slug, self::NON_GPL_PLUGINS, true ) ) {
|
||||||
|
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 [
|
||||||
|
'is_gpl' => true,
|
||||||
|
'confidence' => 100,
|
||||||
|
'source' => 'wordpress_org',
|
||||||
|
'license' => $wporg_result['license'] ?? 'GPL-2.0+',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查插件文件
|
||||||
|
if ( ! empty( $plugin_file ) ) {
|
||||||
|
$file_result = $this->check_plugin_file( $plugin_file );
|
||||||
|
if ( $file_result !== null ) {
|
||||||
|
return $file_result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 无法确定
|
||||||
|
return [
|
||||||
|
'is_gpl' => null,
|
||||||
|
'confidence' => 0,
|
||||||
|
'source' => 'unknown',
|
||||||
|
'license' => 'unknown',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 WordPress.org API
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
private function check_wordpress_org( string $plugin_slug ): ?array {
|
||||||
|
$cache_key = 'wpbridge_wporg_gpl_' . md5( $plugin_slug );
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
|
||||||
|
if ( $cached !== false ) {
|
||||||
|
return $cached === 'not_found' ? null : $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=' . urlencode( $plugin_slug );
|
||||||
|
$response = wp_remote_get( $url, [ 'timeout' => 5 ] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code( $response );
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
|
||||||
|
if ( $code === 200 && ! empty( $body ) ) {
|
||||||
|
$data = json_decode( $body, true );
|
||||||
|
if ( isset( $data['slug'] ) ) {
|
||||||
|
$result = [
|
||||||
|
'license' => 'GPL-2.0+', // WordPress.org 要求 GPL
|
||||||
|
'name' => $data['name'] ?? '',
|
||||||
|
];
|
||||||
|
set_transient( $cache_key, $result, DAY_IN_SECONDS );
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_transient( $cache_key, 'not_found', HOUR_IN_SECONDS );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查插件文件中的授权信息
|
||||||
|
*
|
||||||
|
* @param string $plugin_file 插件文件路径
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
private function check_plugin_file( string $plugin_file ): ?array {
|
||||||
|
$plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
|
||||||
|
|
||||||
|
if ( ! file_exists( $plugin_path ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取插件头部
|
||||||
|
if ( ! function_exists( 'get_plugin_data' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_data = get_plugin_data( $plugin_path, false, false );
|
||||||
|
$license = $plugin_data['License'] ?? '';
|
||||||
|
|
||||||
|
if ( ! empty( $license ) ) {
|
||||||
|
$is_gpl = $this->is_gpl_compatible_license( $license );
|
||||||
|
if ( $is_gpl !== null ) {
|
||||||
|
return [
|
||||||
|
'is_gpl' => $is_gpl,
|
||||||
|
'confidence' => 90,
|
||||||
|
'source' => 'plugin_header',
|
||||||
|
'license' => $license,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 license.txt
|
||||||
|
$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 [
|
||||||
|
'is_gpl' => true,
|
||||||
|
'confidence' => 85,
|
||||||
|
'source' => 'license_file',
|
||||||
|
'license' => 'GPL (from license.txt)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 readme.txt
|
||||||
|
$readme_file = $plugin_dir . '/readme.txt';
|
||||||
|
if ( file_exists( $readme_file ) ) {
|
||||||
|
$readme_content = file_get_contents( $readme_file );
|
||||||
|
if ( preg_match( '/License:\s*(.+)/i', $readme_content, $matches ) ) {
|
||||||
|
$license = trim( $matches[1] );
|
||||||
|
$is_gpl = $this->is_gpl_compatible_license( $license );
|
||||||
|
if ( $is_gpl !== null ) {
|
||||||
|
return [
|
||||||
|
'is_gpl' => $is_gpl,
|
||||||
|
'confidence' => 80,
|
||||||
|
'source' => 'readme_file',
|
||||||
|
'license' => $license,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查授权字符串是否 GPL 兼容
|
||||||
|
*
|
||||||
|
* @param string $license 授权字符串
|
||||||
|
* @return bool|null
|
||||||
|
*/
|
||||||
|
private function is_gpl_compatible_license( string $license ): ?bool {
|
||||||
|
$license_lower = strtolower( trim( $license ) );
|
||||||
|
|
||||||
|
foreach ( self::GPL_COMPATIBLE_LICENSES as $gpl_license ) {
|
||||||
|
if ( strpos( $license_lower, $gpl_license ) !== false ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查明确的非 GPL 标识
|
||||||
|
$non_gpl_indicators = [ 'proprietary', 'commercial', 'all rights reserved', 'envato' ];
|
||||||
|
foreach ( $non_gpl_indicators as $indicator ) {
|
||||||
|
if ( strpos( $license_lower, $indicator ) !== false ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文本是否包含 GPL 授权内容
|
||||||
|
*
|
||||||
|
* @param string $content 文本内容
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function contains_gpl_text( string $content ): bool {
|
||||||
|
$gpl_indicators = [
|
||||||
|
'GNU General Public License',
|
||||||
|
'GPL version 2',
|
||||||
|
'GPL version 3',
|
||||||
|
'GPLv2',
|
||||||
|
'GPLv3',
|
||||||
|
'free software',
|
||||||
|
'redistribute it and/or modify',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ( $gpl_indicators as $indicator ) {
|
||||||
|
if ( stripos( $content, $indicator ) !== false ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量验证
|
||||||
|
*
|
||||||
|
* @param array $plugins 插件列表 [ slug => file ]
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function validate_batch( array $plugins ): array {
|
||||||
|
$results = [];
|
||||||
|
foreach ( $plugins as $slug => $file ) {
|
||||||
|
$results[ $slug ] = $this->validate( $slug, $file );
|
||||||
|
}
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存
|
||||||
|
*/
|
||||||
|
public function clear_cache(): void {
|
||||||
|
$this->cache = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加到已知 GPL 列表(运行时)
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
*/
|
||||||
|
public function add_known_gpl( string $plugin_slug ): void {
|
||||||
|
$this->cache[ $plugin_slug ] = [
|
||||||
|
'is_gpl' => true,
|
||||||
|
'confidence' => 100,
|
||||||
|
'source' => 'manual',
|
||||||
|
'license' => 'GPL (manually verified)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
572
includes/Commercial/LicenseProxy.php
Normal file
572
includes/Commercial/LicenseProxy.php
Normal file
|
|
@ -0,0 +1,572 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 商业插件授权代理
|
||||||
|
*
|
||||||
|
* 拦截商业插件的授权验证请求,转发到文派授权服务
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.7
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WPBridge\Commercial;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LicenseProxy 类
|
||||||
|
*/
|
||||||
|
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',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 敏感参数列表(用于日志过滤)
|
||||||
|
*/
|
||||||
|
private const SENSITIVE_PARAMS = [
|
||||||
|
'license_key',
|
||||||
|
'license',
|
||||||
|
'key',
|
||||||
|
'password',
|
||||||
|
'secret',
|
||||||
|
'token',
|
||||||
|
'api_key',
|
||||||
|
'apikey',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已桥接的插件列表
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $bridged_plugins = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
*/
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否启用
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function is_enabled(): bool {
|
||||||
|
return (bool) $this->settings->get( 'license_proxy_enabled', false );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拦截 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 ) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// H2 修复: 过滤敏感信息后再记录日志
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H2 修复: 过滤 URL 中的敏感参数
|
||||||
|
*
|
||||||
|
* @param string $url 原始 URL
|
||||||
|
* @return string 过滤后的 URL
|
||||||
|
*/
|
||||||
|
private function sanitize_url_for_log( string $url ): string {
|
||||||
|
$pattern = '/(' . implode( '|', self::SENSITIVE_PARAMS ) . ')=[^&]+/i';
|
||||||
|
return preg_replace( $pattern, '$1=[REDACTED]', $url );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H2 修复: 过滤请求体中的敏感参数
|
||||||
|
*
|
||||||
|
* @param array $body 请求体
|
||||||
|
* @return array 过滤后的请求体
|
||||||
|
*/
|
||||||
|
private function sanitize_body_for_log( array $body ): array {
|
||||||
|
$sanitized = [];
|
||||||
|
foreach ( $body as $key => $value ) {
|
||||||
|
if ( in_array( strtolower( $key ), self::SENSITIVE_PARAMS, true ) ) {
|
||||||
|
$sanitized[ $key ] = '[REDACTED]';
|
||||||
|
} else {
|
||||||
|
$sanitized[ $key ] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测授权系统供应商
|
||||||
|
*
|
||||||
|
* @param string $url 请求 URL
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* @param string $url 请求 URL
|
||||||
|
* @param array $args 请求参数
|
||||||
|
* @param string $vendor 供应商
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
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( (string) $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( (string) $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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 item_id 到 slug
|
||||||
|
*
|
||||||
|
* @param string $item_id 项目 ID
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
private function resolve_item_id( string $item_id ): ?string {
|
||||||
|
// 从远程配置获取 ID 到 slug 的映射
|
||||||
|
$mapping = $this->settings->get( 'item_id_mapping', [] );
|
||||||
|
return $mapping[ $item_id ] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Freemius ID 到 slug
|
||||||
|
*
|
||||||
|
* @param string $freemius_id Freemius ID
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
private function resolve_freemius_id( string $freemius_id ): ?string {
|
||||||
|
$mapping = $this->settings->get( 'freemius_id_mapping', [] );
|
||||||
|
return $mapping[ $freemius_id ] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查插件是否在桥接列表
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function is_bridged( string $plugin_slug ): bool {
|
||||||
|
return in_array( $plugin_slug, $this->bridged_plugins, true );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H1 修复: 生成安全的站点指纹
|
||||||
|
*
|
||||||
|
* 使用多因素生成站点指纹,防止伪造
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function generate_site_fingerprint(): string {
|
||||||
|
$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 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H3 修复: 生成请求签名
|
||||||
|
*
|
||||||
|
* @param string $api_key API Key
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param string $action 操作类型
|
||||||
|
* @param string $timestamp 时间戳
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function generate_request_signature( string $api_key, string $plugin_slug, string $action, string $timestamp ): string {
|
||||||
|
$data = implode( '|', [
|
||||||
|
$plugin_slug,
|
||||||
|
$this->generate_site_fingerprint(),
|
||||||
|
$action,
|
||||||
|
$timestamp,
|
||||||
|
] );
|
||||||
|
|
||||||
|
return hash_hmac( 'sha256', $data, $api_key );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理请求到文派服务
|
||||||
|
*
|
||||||
|
* @param string $vendor 供应商
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param string $original_url 原始 URL
|
||||||
|
* @param array $args 请求参数
|
||||||
|
* @return array|false
|
||||||
|
*/
|
||||||
|
private function proxy_request( string $vendor, string $plugin_slug, string $original_url, array $args ) {
|
||||||
|
$proxy_url = $this->settings->get(
|
||||||
|
'license_proxy_url',
|
||||||
|
'https://updates.wenpai.net/api/v1/license/proxy'
|
||||||
|
);
|
||||||
|
|
||||||
|
$api_key = $this->get_api_key();
|
||||||
|
$timestamp = (string) time();
|
||||||
|
$action = $this->extract_action( $original_url, $args );
|
||||||
|
|
||||||
|
// H1 + H3 修复: 使用站点指纹和请求签名
|
||||||
|
$site_fingerprint = $this->generate_site_fingerprint();
|
||||||
|
$signature = $this->generate_request_signature( $api_key, $plugin_slug, $action, $timestamp );
|
||||||
|
|
||||||
|
$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', [
|
||||||
|
'error' => $response->get_error_message(),
|
||||||
|
] );
|
||||||
|
// 失败时不拦截,让原始请求继续
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换响应格式
|
||||||
|
return $this->transform_response( $vendor, $plugin_slug, $response );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H4 修复: 转换响应格式以匹配原厂 API
|
||||||
|
*
|
||||||
|
* @param string $vendor 供应商
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param array $response 响应
|
||||||
|
* @return array|false
|
||||||
|
*/
|
||||||
|
private function transform_response( string $vendor, string $plugin_slug, array $response ) {
|
||||||
|
$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, $plugin_slug );
|
||||||
|
case 'freemius':
|
||||||
|
return $this->format_freemius_response( $license, $plugin_slug );
|
||||||
|
case 'wc_am':
|
||||||
|
return $this->format_wc_am_response( $license );
|
||||||
|
default:
|
||||||
|
return $this->format_generic_response( $license );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H4 修复: 格式化 EDD 响应(完整字段)
|
||||||
|
*
|
||||||
|
* @param array $license 授权信息
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function format_edd_response( array $license, string $plugin_slug ): array {
|
||||||
|
// 获取插件特定的响应配置
|
||||||
|
$plugin_config = $this->get_plugin_response_config( $plugin_slug, 'edd' );
|
||||||
|
|
||||||
|
$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 [
|
||||||
|
'response' => [ 'code' => 200, 'message' => 'OK' ],
|
||||||
|
'body' => $body,
|
||||||
|
'headers' => [ 'content-type' => 'application/json' ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H4 修复: 格式化 Freemius 响应(完整字段)
|
||||||
|
*
|
||||||
|
* @param array $license 授权信息
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
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( [
|
||||||
|
'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 [
|
||||||
|
'response' => [ 'code' => 200, 'message' => 'OK' ],
|
||||||
|
'body' => $body,
|
||||||
|
'headers' => [ 'content-type' => 'application/json' ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化 WC API Manager 响应
|
||||||
|
*
|
||||||
|
* @param array $license 授权信息
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function format_wc_am_response( array $license ): array {
|
||||||
|
$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 [
|
||||||
|
'response' => [ 'code' => 200, 'message' => 'OK' ],
|
||||||
|
'body' => $body,
|
||||||
|
'headers' => [ 'content-type' => 'application/json' ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化通用响应
|
||||||
|
*
|
||||||
|
* @param array $license 授权信息
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function format_generic_response( array $license ): array {
|
||||||
|
$body = wp_json_encode( [
|
||||||
|
'success' => true,
|
||||||
|
'license' => $license['status'] ?? 'valid',
|
||||||
|
'expires' => $license['expires'] ?? 'lifetime',
|
||||||
|
] );
|
||||||
|
|
||||||
|
return [
|
||||||
|
'response' => [ 'code' => 200, 'message' => 'OK' ],
|
||||||
|
'body' => $body,
|
||||||
|
'headers' => [ 'content-type' => 'application/json' ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H4 修复: 获取插件特定的响应配置
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param string $vendor 供应商
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function get_plugin_response_config( string $plugin_slug, string $vendor ): array {
|
||||||
|
$configs = $this->settings->get( 'plugin_response_configs', [] );
|
||||||
|
return $configs[ $plugin_slug ][ $vendor ] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成校验和
|
||||||
|
*
|
||||||
|
* @param array $license 授权信息
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function generate_checksum( array $license ): string {
|
||||||
|
return md5( wp_json_encode( $license ) . home_url() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 Freemius secret_key
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function generate_secret_key(): string {
|
||||||
|
return 'sk_' . bin2hex( random_bytes( 16 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 API Key
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function get_api_key(): string {
|
||||||
|
return $this->settings->get( 'wenpai_api_key', '' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取操作类型
|
||||||
|
*
|
||||||
|
* @param string $url 请求 URL
|
||||||
|
* @param array $args 请求参数
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
269
includes/Commercial/Vendors/AbstractVendor.php
Normal file
269
includes/Commercial/Vendors/AbstractVendor.php
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 供应商抽象基类
|
||||||
|
*
|
||||||
|
* 提供供应商通用功能实现
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.7
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WPBridge\Commercial\Vendors;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbstractVendor 抽象类
|
||||||
|
*/
|
||||||
|
abstract class AbstractVendor implements VendorInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商配置
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected array $config = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存前缀
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected string $cache_prefix = 'wpbridge_vendor_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存时间(秒)
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected int $cache_ttl = 3600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param array $config 配置
|
||||||
|
*/
|
||||||
|
public function __construct( array $config = [] ) {
|
||||||
|
$this->config = array_merge( $this->get_default_config(), $config );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认配置
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function get_default_config(): array {
|
||||||
|
return [
|
||||||
|
'api_url' => '',
|
||||||
|
'api_key' => '',
|
||||||
|
'api_secret' => '',
|
||||||
|
'timeout' => 15,
|
||||||
|
'enabled' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查供应商是否可用
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function is_available(): bool {
|
||||||
|
if ( empty( $this->config['enabled'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $this->config['api_url'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->verify_credentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 API 请求
|
||||||
|
*
|
||||||
|
* @param string $endpoint 端点
|
||||||
|
* @param array $params 参数
|
||||||
|
* @param string $method 方法 (GET/POST)
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
protected function api_request( string $endpoint, array $params = [], string $method = 'GET' ): ?array {
|
||||||
|
$url = trailingslashit( $this->config['api_url'] ) . ltrim( $endpoint, '/' );
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
'timeout' => $this->config['timeout'],
|
||||||
|
'headers' => $this->get_request_headers(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ( $method === 'GET' && ! empty( $params ) ) {
|
||||||
|
$url = add_query_arg( $params, $url );
|
||||||
|
} elseif ( $method === 'POST' ) {
|
||||||
|
$args['body'] = $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $method === 'GET'
|
||||||
|
? wp_remote_get( $url, $args )
|
||||||
|
: wp_remote_post( $url, $args );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
Logger::error( 'Vendor API request failed', [
|
||||||
|
'vendor' => $this->get_id(),
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'error' => $response->get_error_message(),
|
||||||
|
] );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code( $response );
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
|
||||||
|
if ( $code !== 200 ) {
|
||||||
|
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', [
|
||||||
|
'vendor' => $this->get_id(),
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
] );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求头
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function get_request_headers(): array {
|
||||||
|
return [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存
|
||||||
|
*
|
||||||
|
* @param string $key 缓存键
|
||||||
|
* @return mixed|null
|
||||||
|
*/
|
||||||
|
protected function get_cache( string $key ) {
|
||||||
|
$cache_key = $this->cache_prefix . $this->get_id() . '_' . md5( $key );
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
return $cached !== false ? $cached : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存
|
||||||
|
*
|
||||||
|
* @param string $key 缓存键
|
||||||
|
* @param mixed $value 缓存值
|
||||||
|
* @param int $ttl 过期时间(秒)
|
||||||
|
*/
|
||||||
|
protected function set_cache( string $key, $value, int $ttl = 0 ): void {
|
||||||
|
$cache_key = $this->cache_prefix . $this->get_id() . '_' . md5( $key );
|
||||||
|
set_transient( $cache_key, $value, $ttl ?: $this->cache_ttl );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存
|
||||||
|
*
|
||||||
|
* @param string $key 缓存键(可选,为空则清除所有)
|
||||||
|
*/
|
||||||
|
protected function clear_cache( string $key = '' ): void {
|
||||||
|
if ( ! empty( $key ) ) {
|
||||||
|
$cache_key = $this->cache_prefix . $this->get_id() . '_' . md5( $key );
|
||||||
|
delete_transient( $cache_key );
|
||||||
|
}
|
||||||
|
// 清除所有缓存需要遍历,暂不实现
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索插件(默认实现:从列表中过滤)
|
||||||
|
*
|
||||||
|
* @param string $keyword 关键词
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function search_plugins( string $keyword ): array {
|
||||||
|
$all_plugins = $this->get_plugins( 1, 1000 );
|
||||||
|
$keyword = strtolower( $keyword );
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ( $all_plugins['plugins'] as $plugin ) {
|
||||||
|
$name = strtolower( $plugin['name'] ?? '' );
|
||||||
|
$slug = strtolower( $plugin['slug'] ?? '' );
|
||||||
|
|
||||||
|
if ( strpos( $name, $keyword ) !== false || strpos( $slug, $keyword ) !== false ) {
|
||||||
|
$results[] = $plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件详情(默认实现:从列表中查找)
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function get_plugin( string $slug ): ?array {
|
||||||
|
// 先检查缓存
|
||||||
|
$cache_key = 'plugin_' . $slug;
|
||||||
|
$cached = $this->get_cache( $cache_key );
|
||||||
|
if ( $cached !== null ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从列表中查找
|
||||||
|
$all_plugins = $this->get_plugins( 1, 1000 );
|
||||||
|
foreach ( $all_plugins['plugins'] as $plugin ) {
|
||||||
|
if ( ( $plugin['slug'] ?? '' ) === $slug ) {
|
||||||
|
$this->set_cache( $cache_key, $plugin );
|
||||||
|
return $plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化插件数据
|
||||||
|
*
|
||||||
|
* @param array $raw_plugin 原始插件数据
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function normalize_plugin( array $raw_plugin ): array {
|
||||||
|
return [
|
||||||
|
'slug' => $raw_plugin['slug'] ?? '',
|
||||||
|
'name' => $raw_plugin['name'] ?? $raw_plugin['title'] ?? '',
|
||||||
|
'version' => $raw_plugin['version'] ?? '',
|
||||||
|
'author' => $raw_plugin['author'] ?? '',
|
||||||
|
'description' => $raw_plugin['description'] ?? $raw_plugin['excerpt'] ?? '',
|
||||||
|
'homepage' => $raw_plugin['homepage'] ?? $raw_plugin['url'] ?? '',
|
||||||
|
'download_url' => $raw_plugin['download_url'] ?? $raw_plugin['download_link'] ?? '',
|
||||||
|
'tested' => $raw_plugin['tested'] ?? '',
|
||||||
|
'requires' => $raw_plugin['requires'] ?? $raw_plugin['requires_at_least'] ?? '',
|
||||||
|
'requires_php' => $raw_plugin['requires_php'] ?? '',
|
||||||
|
'last_updated' => $raw_plugin['last_updated'] ?? $raw_plugin['modified'] ?? '',
|
||||||
|
'vendor' => $this->get_id(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
112
includes/Commercial/Vendors/VendorInterface.php
Normal file
112
includes/Commercial/Vendors/VendorInterface.php
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 供应商接口
|
||||||
|
*
|
||||||
|
* 定义第三方 GPL 插件供应商的标准接口
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.7
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WPBridge\Commercial\Vendors;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VendorInterface 接口
|
||||||
|
*/
|
||||||
|
interface VendorInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商唯一标识
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_id(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商信息
|
||||||
|
*
|
||||||
|
* @return array {
|
||||||
|
* @type string $id 供应商 ID
|
||||||
|
* @type string $name 供应商名称
|
||||||
|
* @type string $url 供应商网站
|
||||||
|
* @type string $api_type API 类型 (wc_am, edd, custom)
|
||||||
|
* @type bool $requires_key 是否需要 API Key
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function get_info(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查供应商是否可用
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function is_available(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可用插件列表
|
||||||
|
*
|
||||||
|
* @param int $page 页码
|
||||||
|
* @param int $limit 每页数量
|
||||||
|
* @return array {
|
||||||
|
* @type array[] $plugins 插件列表
|
||||||
|
* @type int $total 总数
|
||||||
|
* @type int $pages 总页数
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function get_plugins( int $page = 1, int $limit = 100 ): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索插件
|
||||||
|
*
|
||||||
|
* @param string $keyword 关键词
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function search_plugins( string $keyword ): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件详情
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function get_plugin( string $slug ): ?array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查插件更新
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @param string $current_version 当前版本
|
||||||
|
* @return array|null {
|
||||||
|
* @type string $version 最新版本
|
||||||
|
* @type string $download_url 下载链接
|
||||||
|
* @type string $changelog 更新日志
|
||||||
|
* @type string $tested 测试的 WP 版本
|
||||||
|
* @type string $requires 最低 WP 版本
|
||||||
|
* @type string $requires_php 最低 PHP 版本
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function check_update( string $slug, string $current_version ): ?array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载链接
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @param string $version 版本号(可选,默认最新)
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function get_download_url( string $slug, string $version = '' ): ?string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证供应商授权
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function verify_credentials(): bool;
|
||||||
|
}
|
||||||
374
includes/Commercial/Vendors/VendorManager.php
Normal file
374
includes/Commercial/Vendors/VendorManager.php
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 供应商管理器
|
||||||
|
*
|
||||||
|
* 管理所有第三方 GPL 插件供应商
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.7
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WPBridge\Commercial\Vendors;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VendorManager 类
|
||||||
|
*/
|
||||||
|
class VendorManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已注册的供应商
|
||||||
|
*
|
||||||
|
* @var VendorInterface[]
|
||||||
|
*/
|
||||||
|
private array $vendors = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单例实例
|
||||||
|
*
|
||||||
|
* @var VendorManager|null
|
||||||
|
*/
|
||||||
|
private static ?VendorManager $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单例实例
|
||||||
|
*
|
||||||
|
* @param Settings|null $settings 设置实例
|
||||||
|
* @return VendorManager
|
||||||
|
*/
|
||||||
|
public static function get_instance( ?Settings $settings = null ): VendorManager {
|
||||||
|
if ( self::$instance === null ) {
|
||||||
|
self::$instance = new self( $settings ?? new Settings() );
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
*/
|
||||||
|
public function __construct( Settings $settings ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->load_vendors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载已配置的供应商
|
||||||
|
*/
|
||||||
|
private function load_vendors(): void {
|
||||||
|
$vendor_configs = $this->settings->get( 'vendors', [] );
|
||||||
|
|
||||||
|
foreach ( $vendor_configs as $vendor_id => $config ) {
|
||||||
|
if ( empty( $config['enabled'] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$vendor = $this->create_vendor( $vendor_id, $config );
|
||||||
|
if ( $vendor !== null ) {
|
||||||
|
$this->vendors[ $vendor_id ] = $vendor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 允许通过 hook 注册额外供应商
|
||||||
|
do_action( 'wpbridge_register_vendors', $this );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建供应商实例
|
||||||
|
*
|
||||||
|
* @param string $vendor_id 供应商 ID
|
||||||
|
* @param array $config 配置
|
||||||
|
* @return VendorInterface|null
|
||||||
|
*/
|
||||||
|
private function create_vendor( string $vendor_id, array $config ): ?VendorInterface {
|
||||||
|
$type = $config['type'] ?? 'woocommerce';
|
||||||
|
$name = $config['name'] ?? $vendor_id;
|
||||||
|
|
||||||
|
switch ( $type ) {
|
||||||
|
case 'woocommerce':
|
||||||
|
case 'wc_am':
|
||||||
|
return new WooCommerceVendor( $vendor_id, $name, $config );
|
||||||
|
|
||||||
|
// 未来可以添加更多类型
|
||||||
|
// case 'edd':
|
||||||
|
// return new EDDVendor($vendor_id, $name, $config);
|
||||||
|
|
||||||
|
default:
|
||||||
|
Logger::warning( 'Unknown vendor type', [
|
||||||
|
'vendor_id' => $vendor_id,
|
||||||
|
'type' => $type,
|
||||||
|
] );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册供应商
|
||||||
|
*
|
||||||
|
* @param VendorInterface $vendor 供应商实例
|
||||||
|
*/
|
||||||
|
public function register( VendorInterface $vendor ): void {
|
||||||
|
$this->vendors[ $vendor->get_id() ] = $vendor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商
|
||||||
|
*
|
||||||
|
* @param string $vendor_id 供应商 ID
|
||||||
|
* @return VendorInterface|null
|
||||||
|
*/
|
||||||
|
public function get_vendor( string $vendor_id ): ?VendorInterface {
|
||||||
|
return $this->vendors[ $vendor_id ] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有供应商
|
||||||
|
*
|
||||||
|
* @param bool $only_available 只返回可用的供应商
|
||||||
|
* @return VendorInterface[]
|
||||||
|
*/
|
||||||
|
public function get_vendors( bool $only_available = false ): array {
|
||||||
|
if ( ! $only_available ) {
|
||||||
|
return $this->vendors;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter(
|
||||||
|
$this->vendors,
|
||||||
|
fn( VendorInterface $vendor ) => $vendor->is_available()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有供应商信息
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_vendors_info(): array {
|
||||||
|
$info = [];
|
||||||
|
foreach ( $this->vendors as $vendor ) {
|
||||||
|
$info[ $vendor->get_id() ] = array_merge(
|
||||||
|
$vendor->get_info(),
|
||||||
|
[ 'available' => $vendor->is_available() ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从所有供应商搜索插件
|
||||||
|
*
|
||||||
|
* @param string $keyword 关键词
|
||||||
|
* @param string $vendor_id 指定供应商(可选)
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function search_plugins( string $keyword, string $vendor_id = '' ): array {
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
$vendors = ! empty( $vendor_id )
|
||||||
|
? [ $this->get_vendor( $vendor_id ) ]
|
||||||
|
: $this->get_vendors( true );
|
||||||
|
|
||||||
|
foreach ( $vendors as $vendor ) {
|
||||||
|
if ( $vendor === null ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$vendor_results = $vendor->search_plugins( $keyword );
|
||||||
|
foreach ( $vendor_results as $plugin ) {
|
||||||
|
$plugin['vendor_id'] = $vendor->get_id();
|
||||||
|
$plugin['vendor_name'] = $vendor->get_info()['name'] ?? '';
|
||||||
|
$results[] = $plugin;
|
||||||
|
}
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
Logger::error( 'Vendor search failed', [
|
||||||
|
'vendor' => $vendor->get_id(),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从所有供应商获取插件
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return array|null 包含插件信息和供应商信息
|
||||||
|
*/
|
||||||
|
public function get_plugin( string $slug ): ?array {
|
||||||
|
foreach ( $this->get_vendors( true ) as $vendor ) {
|
||||||
|
$plugin = $vendor->get_plugin( $slug );
|
||||||
|
if ( $plugin !== null ) {
|
||||||
|
$plugin['vendor_id'] = $vendor->get_id();
|
||||||
|
$plugin['vendor_name'] = $vendor->get_info()['name'] ?? '';
|
||||||
|
return $plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查插件更新(从所有供应商)
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @param string $current_version 当前版本
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function check_update( string $slug, string $current_version ): ?array {
|
||||||
|
foreach ( $this->get_vendors( true ) as $vendor ) {
|
||||||
|
$update = $vendor->check_update( $slug, $current_version );
|
||||||
|
if ( $update !== null ) {
|
||||||
|
$update['vendor_id'] = $vendor->get_id();
|
||||||
|
$update['vendor_name'] = $vendor->get_info()['name'] ?? '';
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载链接
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @param string $vendor_id 供应商 ID(可选,自动查找)
|
||||||
|
* @param string $version 版本号(可选)
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function get_download_url( string $slug, string $vendor_id = '', string $version = '' ): ?string {
|
||||||
|
if ( ! empty( $vendor_id ) ) {
|
||||||
|
$vendor = $this->get_vendor( $vendor_id );
|
||||||
|
if ( $vendor !== null ) {
|
||||||
|
return $vendor->get_download_url( $slug, $version );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动查找
|
||||||
|
foreach ( $this->get_vendors( true ) as $vendor ) {
|
||||||
|
$plugin = $vendor->get_plugin( $slug );
|
||||||
|
if ( $plugin !== null ) {
|
||||||
|
return $vendor->get_download_url( $slug, $version );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加供应商配置
|
||||||
|
*
|
||||||
|
* @param string $vendor_id 供应商 ID
|
||||||
|
* @param array $config 配置
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function add_vendor_config( string $vendor_id, array $config ): bool {
|
||||||
|
$vendors = $this->settings->get( 'vendors', [] );
|
||||||
|
$vendors[ $vendor_id ] = $config;
|
||||||
|
|
||||||
|
$result = $this->settings->set( 'vendors', $vendors );
|
||||||
|
|
||||||
|
if ( $result ) {
|
||||||
|
// 重新加载
|
||||||
|
$vendor = $this->create_vendor( $vendor_id, $config );
|
||||||
|
if ( $vendor !== null ) {
|
||||||
|
$this->vendors[ $vendor_id ] = $vendor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除供应商配置
|
||||||
|
*
|
||||||
|
* @param string $vendor_id 供应商 ID
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function remove_vendor_config( string $vendor_id ): bool {
|
||||||
|
$vendors = $this->settings->get( 'vendors', [] );
|
||||||
|
|
||||||
|
if ( ! isset( $vendors[ $vendor_id ] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset( $vendors[ $vendor_id ] );
|
||||||
|
unset( $this->vendors[ $vendor_id ] );
|
||||||
|
|
||||||
|
return $this->settings->set( 'vendors', $vendors );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试供应商连接
|
||||||
|
*
|
||||||
|
* @param string $vendor_id 供应商 ID
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function test_vendor( string $vendor_id ): array {
|
||||||
|
$vendor = $this->get_vendor( $vendor_id );
|
||||||
|
|
||||||
|
if ( $vendor === null ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => __( '供应商不存在', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$available = $vendor->is_available();
|
||||||
|
|
||||||
|
if ( ! $available ) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => __( '供应商连接失败,请检查配置', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取插件列表
|
||||||
|
$plugins = $vendor->get_plugins( 1, 10 );
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => __( '连接成功', 'wpbridge' ),
|
||||||
|
'plugin_count' => $plugins['total'] ?? count( $plugins['plugins'] ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统计信息
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_stats(): array {
|
||||||
|
$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 );
|
||||||
|
$total_plugins += $plugins['total'] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_vendors' => $total_vendors,
|
||||||
|
'active_vendors' => $active_vendors,
|
||||||
|
'total_plugins' => $total_plugins,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
465
includes/Commercial/Vendors/WooCommerceVendor.php
Normal file
465
includes/Commercial/Vendors/WooCommerceVendor.php
Normal file
|
|
@ -0,0 +1,465 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WooCommerce API Manager 供应商
|
||||||
|
*
|
||||||
|
* 支持使用 WooCommerce API Manager 的 GPL 插件商店
|
||||||
|
* 大部分 GPL 分发商店都使用此方案
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.7
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WPBridge\Commercial\Vendors;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WooCommerceVendor 类
|
||||||
|
*
|
||||||
|
* 支持的 API 端点:
|
||||||
|
* - WooCommerce API Manager v2
|
||||||
|
* - WooCommerce Software Add-on
|
||||||
|
* - 类似的 WC 扩展
|
||||||
|
*/
|
||||||
|
class WooCommerceVendor extends AbstractVendor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商 ID
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected string $vendor_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商名称
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected string $vendor_name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param string $vendor_id 供应商 ID
|
||||||
|
* @param string $vendor_name 供应商名称
|
||||||
|
* @param array $config 配置
|
||||||
|
*/
|
||||||
|
public function __construct( string $vendor_id, string $vendor_name, array $config = [] ) {
|
||||||
|
$this->vendor_id = $vendor_id;
|
||||||
|
$this->vendor_name = $vendor_name;
|
||||||
|
parent::__construct( $config );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认配置
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function get_default_config(): array {
|
||||||
|
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', // 下载端点
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商 ID
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_id(): string {
|
||||||
|
return $this->vendor_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商信息
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_info(): 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求头
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function get_request_headers(): array {
|
||||||
|
$headers = parent::get_request_headers();
|
||||||
|
|
||||||
|
// WooCommerce REST API 认证
|
||||||
|
if ( ! empty( $this->config['api_key'] ) && ! empty( $this->config['api_secret'] ) ) {
|
||||||
|
$headers['Authorization'] = 'Basic ' . base64_encode(
|
||||||
|
$this->config['api_key'] . ':' . $this->config['api_secret']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证供应商授权
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function verify_credentials(): bool {
|
||||||
|
$cache_key = 'credentials_valid';
|
||||||
|
$cached = $this->get_cache( $cache_key );
|
||||||
|
|
||||||
|
if ( $cached !== null ) {
|
||||||
|
return (bool) $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取产品列表来验证
|
||||||
|
$response = $this->api_request( $this->config['products_endpoint'], [
|
||||||
|
'per_page' => 1,
|
||||||
|
'status' => 'publish',
|
||||||
|
] );
|
||||||
|
|
||||||
|
$valid = $response !== null;
|
||||||
|
$this->set_cache( $cache_key, $valid, 300 ); // 5分钟缓存
|
||||||
|
|
||||||
|
return $valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可用插件列表
|
||||||
|
*
|
||||||
|
* @param int $page 页码
|
||||||
|
* @param int $limit 每页数量
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_plugins( int $page = 1, int $limit = 100 ): array {
|
||||||
|
$cache_key = "plugins_page_{$page}_limit_{$limit}";
|
||||||
|
$cached = $this->get_cache( $cache_key );
|
||||||
|
|
||||||
|
if ( $cached !== null ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 [
|
||||||
|
'plugins' => [],
|
||||||
|
'total' => 0,
|
||||||
|
'pages' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugins = [];
|
||||||
|
foreach ( $response as $product ) {
|
||||||
|
$plugin = $this->normalize_wc_product( $product );
|
||||||
|
if ( $plugin !== null ) {
|
||||||
|
$plugins[] = $plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 );
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化 WooCommerce 产品数据
|
||||||
|
*
|
||||||
|
* @param array $product WC 产品数据
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
protected function normalize_wc_product( array $product ): ?array {
|
||||||
|
// 跳过非插件产品
|
||||||
|
if ( ! $this->is_plugin_product( $product ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从产品数据中提取插件信息
|
||||||
|
$slug = $this->extract_plugin_slug( $product );
|
||||||
|
if ( empty( $slug ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'slug' => $slug,
|
||||||
|
'name' => $product['name'] ?? '',
|
||||||
|
'version' => $this->extract_version( $product ),
|
||||||
|
'author' => $this->extract_author( $product ),
|
||||||
|
'description' => wp_strip_all_tags( $product['short_description'] ?? $product['description'] ?? '' ),
|
||||||
|
'homepage' => $product['permalink'] ?? '',
|
||||||
|
'download_url' => '', // 需要单独获取
|
||||||
|
'tested' => $this->extract_meta( $product, '_tested_wp_version' ),
|
||||||
|
'requires' => $this->extract_meta( $product, '_requires_wp_version' ),
|
||||||
|
'requires_php' => $this->extract_meta( $product, '_requires_php_version' ),
|
||||||
|
'last_updated' => $product['date_modified'] ?? '',
|
||||||
|
'price' => $product['price'] ?? '0',
|
||||||
|
'product_id' => $product['id'] ?? 0,
|
||||||
|
'vendor' => $this->get_id(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查产品是否是插件
|
||||||
|
*
|
||||||
|
* @param array $product 产品数据
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function is_plugin_product( array $product ): bool {
|
||||||
|
// 检查分类
|
||||||
|
$categories = $product['categories'] ?? [];
|
||||||
|
foreach ( $categories as $cat ) {
|
||||||
|
$cat_slug = strtolower( $cat['slug'] ?? '' );
|
||||||
|
if ( in_array( $cat_slug, [ 'plugins', 'wordpress-plugins', 'wp-plugins' ], true ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查标签
|
||||||
|
$tags = $product['tags'] ?? [];
|
||||||
|
foreach ( $tags as $tag ) {
|
||||||
|
$tag_slug = strtolower( $tag['slug'] ?? '' );
|
||||||
|
if ( strpos( $tag_slug, 'plugin' ) !== false ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有下载文件
|
||||||
|
$downloads = $product['downloads'] ?? [];
|
||||||
|
foreach ( $downloads as $download ) {
|
||||||
|
$file = strtolower( $download['file'] ?? '' );
|
||||||
|
if ( strpos( $file, '.zip' ) !== false ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从产品中提取插件 slug
|
||||||
|
*
|
||||||
|
* @param array $product 产品数据
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function extract_plugin_slug( array $product ): string {
|
||||||
|
// 1. 从 meta 中获取
|
||||||
|
$slug = $this->extract_meta( $product, '_plugin_slug' );
|
||||||
|
if ( ! empty( $slug ) ) {
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 从 SKU 获取
|
||||||
|
$sku = $product['sku'] ?? '';
|
||||||
|
if ( ! empty( $sku ) ) {
|
||||||
|
return sanitize_title( $sku );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 从产品 slug 获取
|
||||||
|
$product_slug = $product['slug'] ?? '';
|
||||||
|
if ( ! empty( $product_slug ) ) {
|
||||||
|
// 移除常见后缀
|
||||||
|
$product_slug = preg_replace( '/-(pro|premium|plus|addon)$/', '', $product_slug );
|
||||||
|
return $product_slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 从名称生成
|
||||||
|
return sanitize_title( $product['name'] ?? '' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从产品中提取版本号
|
||||||
|
*
|
||||||
|
* @param array $product 产品数据
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function extract_version( array $product ): string {
|
||||||
|
// 从 meta 获取
|
||||||
|
$version = $this->extract_meta( $product, '_version' );
|
||||||
|
if ( ! empty( $version ) ) {
|
||||||
|
return $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
$version = $this->extract_meta( $product, '_plugin_version' );
|
||||||
|
if ( ! empty( $version ) ) {
|
||||||
|
return $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从下载文件名提取
|
||||||
|
$downloads = $product['downloads'] ?? [];
|
||||||
|
foreach ( $downloads as $download ) {
|
||||||
|
$file = $download['file'] ?? '';
|
||||||
|
if ( preg_match( '/[\-_]v?(\d+\.\d+(?:\.\d+)?)/i', $file, $matches ) ) {
|
||||||
|
return $matches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从产品中提取作者
|
||||||
|
*
|
||||||
|
* @param array $product 产品数据
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function extract_author( array $product ): string {
|
||||||
|
$author = $this->extract_meta( $product, '_plugin_author' );
|
||||||
|
if ( ! empty( $author ) ) {
|
||||||
|
return $author;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用商店名称作为默认作者
|
||||||
|
return $this->vendor_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从产品 meta 中提取值
|
||||||
|
*
|
||||||
|
* @param array $product 产品数据
|
||||||
|
* @param string $meta_key Meta 键
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function extract_meta( array $product, string $meta_key ): string {
|
||||||
|
$meta_data = $product['meta_data'] ?? [];
|
||||||
|
foreach ( $meta_data as $meta ) {
|
||||||
|
if ( ( $meta['key'] ?? '' ) === $meta_key ) {
|
||||||
|
return (string) ( $meta['value'] ?? '' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查插件更新
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @param string $current_version 当前版本
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function check_update( string $slug, string $current_version ): ?array {
|
||||||
|
$plugin = $this->get_plugin( $slug );
|
||||||
|
|
||||||
|
if ( $plugin === null ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$latest_version = $plugin['version'] ?? '';
|
||||||
|
|
||||||
|
if ( empty( $latest_version ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( version_compare( $latest_version, $current_version, '<=' ) ) {
|
||||||
|
return null; // 无更新
|
||||||
|
}
|
||||||
|
|
||||||
|
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'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下载链接
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @param string $version 版本号
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function get_download_url( string $slug, string $version = '' ): ?string {
|
||||||
|
$plugin = $this->get_plugin( $slug );
|
||||||
|
|
||||||
|
if ( $plugin === null ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product_id = $plugin['product_id'] ?? 0;
|
||||||
|
|
||||||
|
if ( empty( $product_id ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 WC API Manager 下载链接
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$download_url = add_query_arg(
|
||||||
|
$params,
|
||||||
|
trailingslashit( $this->config['api_url'] ) . ltrim( $this->config['download_endpoint'], '/' )
|
||||||
|
);
|
||||||
|
|
||||||
|
return $download_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取更新日志
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function get_changelog( string $slug ): string {
|
||||||
|
$plugin = $this->get_plugin( $slug );
|
||||||
|
|
||||||
|
if ( $plugin === null ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从产品描述中提取 changelog
|
||||||
|
$description = $plugin['description'] ?? '';
|
||||||
|
|
||||||
|
if ( preg_match( '/changelog[:\s]*(.+?)(?=<h|$)/is', $description, $matches ) ) {
|
||||||
|
return wp_strip_all_tags( $matches[1] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成实例 ID
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function generate_instance_id(): string {
|
||||||
|
return md5( home_url() . AUTH_KEY );
|
||||||
|
}
|
||||||
|
}
|
||||||
502
includes/Core/BackupManager.php
Normal file
502
includes/Core/BackupManager.php
Normal file
|
|
@ -0,0 +1,502 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 备份管理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备份管理器类
|
||||||
|
*/
|
||||||
|
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', [ $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
598
includes/Core/ChangelogManager.php
Normal file
598
includes/Core/ChangelogManager.php
Normal file
|
|
@ -0,0 +1,598 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 更新日志管理器
|
||||||
|
*
|
||||||
|
* 聚合显示插件/主题的更新日志
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新日志管理类
|
||||||
|
*/
|
||||||
|
class ChangelogManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单例实例
|
||||||
|
*
|
||||||
|
* @var self|null
|
||||||
|
*/
|
||||||
|
private static ?self $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存前缀
|
||||||
|
*/
|
||||||
|
const CACHE_PREFIX = 'wpbridge_changelog_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存时间(秒)
|
||||||
|
*/
|
||||||
|
const CACHE_TTL = 3600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单例实例
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function get_instance(): self {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件更新日志
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @param string $source_type 源类型(wporg, custom, git)
|
||||||
|
* @param string $source_url 自定义源 URL(可选)
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_plugin_changelog( string $slug, string $source_type = 'wporg', string $source_url = '' ): array {
|
||||||
|
$cache_key = self::CACHE_PREFIX . 'plugin_' . md5( $slug . $source_type . $source_url );
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
|
||||||
|
if ( false !== $cached ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changelog = [];
|
||||||
|
|
||||||
|
switch ( $source_type ) {
|
||||||
|
case 'wporg':
|
||||||
|
$changelog = $this->fetch_wporg_plugin_changelog( $slug );
|
||||||
|
break;
|
||||||
|
case 'git':
|
||||||
|
case 'github':
|
||||||
|
case 'gitea':
|
||||||
|
$changelog = $this->fetch_git_changelog( $source_url );
|
||||||
|
break;
|
||||||
|
case 'json':
|
||||||
|
case 'custom':
|
||||||
|
$changelog = $this->fetch_custom_changelog( $source_url, 'plugin', $slug );
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$changelog = $this->fetch_wporg_plugin_changelog( $slug );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $changelog ) ) {
|
||||||
|
set_transient( $cache_key, $changelog, self::CACHE_TTL );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $changelog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主题更新日志
|
||||||
|
*
|
||||||
|
* @param string $slug 主题 slug
|
||||||
|
* @param string $source_type 源类型
|
||||||
|
* @param string $source_url 自定义源 URL
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_theme_changelog( string $slug, string $source_type = 'wporg', string $source_url = '' ): array {
|
||||||
|
$cache_key = self::CACHE_PREFIX . 'theme_' . md5( $slug . $source_type . $source_url );
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
|
||||||
|
if ( false !== $cached ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changelog = [];
|
||||||
|
|
||||||
|
switch ( $source_type ) {
|
||||||
|
case 'wporg':
|
||||||
|
$changelog = $this->fetch_wporg_theme_changelog( $slug );
|
||||||
|
break;
|
||||||
|
case 'git':
|
||||||
|
case 'github':
|
||||||
|
case 'gitea':
|
||||||
|
$changelog = $this->fetch_git_changelog( $source_url );
|
||||||
|
break;
|
||||||
|
case 'json':
|
||||||
|
case 'custom':
|
||||||
|
$changelog = $this->fetch_custom_changelog( $source_url, 'theme', $slug );
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$changelog = $this->fetch_wporg_theme_changelog( $slug );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $changelog ) ) {
|
||||||
|
set_transient( $cache_key, $changelog, self::CACHE_TTL );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $changelog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 WordPress.org 获取插件更新日志
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function fetch_wporg_plugin_changelog( string $slug ): array {
|
||||||
|
$api_url = 'https://api.wordpress.org/plugins/info/1.2/';
|
||||||
|
$response = wp_remote_post( $api_url, [
|
||||||
|
'timeout' => 15,
|
||||||
|
'body' => [
|
||||||
|
'action' => 'plugin_information',
|
||||||
|
'request' => serialize( (object) [
|
||||||
|
'slug' => $slug,
|
||||||
|
'fields' => [
|
||||||
|
'sections' => true,
|
||||||
|
'versions' => true,
|
||||||
|
],
|
||||||
|
] ),
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return $this->error_response( $response->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
$data = maybe_unserialize( $body );
|
||||||
|
|
||||||
|
if ( ! is_object( $data ) || isset( $data->error ) ) {
|
||||||
|
return $this->error_response( __( '无法获取插件信息', 'wpbridge' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->format_wporg_changelog( $data, 'plugin' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 WordPress.org 获取主题更新日志
|
||||||
|
*
|
||||||
|
* @param string $slug 主题 slug
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function fetch_wporg_theme_changelog( string $slug ): array {
|
||||||
|
$api_url = 'https://api.wordpress.org/themes/info/1.2/';
|
||||||
|
$response = wp_remote_post( $api_url, [
|
||||||
|
'timeout' => 15,
|
||||||
|
'body' => [
|
||||||
|
'action' => 'theme_information',
|
||||||
|
'request' => serialize( (object) [
|
||||||
|
'slug' => $slug,
|
||||||
|
'fields' => [
|
||||||
|
'sections' => true,
|
||||||
|
'versions' => true,
|
||||||
|
],
|
||||||
|
] ),
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return $this->error_response( $response->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
$data = maybe_unserialize( $body );
|
||||||
|
|
||||||
|
if ( ! is_object( $data ) || isset( $data->error ) ) {
|
||||||
|
return $this->error_response( __( '无法获取主题信息', 'wpbridge' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->format_wporg_changelog( $data, 'theme' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化 WordPress.org 更新日志
|
||||||
|
*
|
||||||
|
* @param object $data API 返回数据
|
||||||
|
* @param string $type 类型(plugin/theme)
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function format_wporg_changelog( object $data, string $type ): array {
|
||||||
|
$result = [
|
||||||
|
'success' => true,
|
||||||
|
'source' => 'WordPress.org',
|
||||||
|
'name' => $data->name ?? '',
|
||||||
|
'slug' => $data->slug ?? '',
|
||||||
|
'version' => $data->version ?? '',
|
||||||
|
'last_updated' => $data->last_updated ?? '',
|
||||||
|
'changelog_html' => '',
|
||||||
|
'versions' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 提取 changelog 部分
|
||||||
|
if ( isset( $data->sections['changelog'] ) ) {
|
||||||
|
$result['changelog_html'] = $data->sections['changelog'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取版本历史
|
||||||
|
if ( isset( $data->versions ) && is_array( $data->versions ) ) {
|
||||||
|
$versions = array_keys( $data->versions );
|
||||||
|
rsort( $versions, SORT_NATURAL );
|
||||||
|
$result['versions'] = array_slice( $versions, 0, 20 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Git 仓库获取更新日志
|
||||||
|
*
|
||||||
|
* @param string $url 仓库 URL
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function fetch_git_changelog( string $url ): array {
|
||||||
|
$parsed = $this->parse_git_url( $url );
|
||||||
|
|
||||||
|
if ( ! $parsed ) {
|
||||||
|
return $this->error_response( __( '无效的 Git 仓库 URL', 'wpbridge' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( $parsed['platform'] ) {
|
||||||
|
case 'github':
|
||||||
|
return $this->fetch_github_releases( $parsed['owner'], $parsed['repo'] );
|
||||||
|
case 'gitea':
|
||||||
|
case 'wenpai':
|
||||||
|
return $this->fetch_gitea_releases( $parsed['base_url'], $parsed['owner'], $parsed['repo'] );
|
||||||
|
default:
|
||||||
|
return $this->error_response( __( '不支持的 Git 平台', 'wpbridge' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Git URL
|
||||||
|
*
|
||||||
|
* @param string $url URL
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
private function parse_git_url( string $url ): ?array {
|
||||||
|
// GitHub
|
||||||
|
if ( preg_match( '#github\.com/([^/]+)/([^/]+)#', $url, $matches ) ) {
|
||||||
|
return [
|
||||||
|
'platform' => 'github',
|
||||||
|
'owner' => $matches[1],
|
||||||
|
'repo' => rtrim( $matches[2], '.git' ),
|
||||||
|
'base_url' => 'https://api.github.com',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菲码源库 (git.wenpai.org)
|
||||||
|
if ( preg_match( '#git\.wenpai\.org/([^/]+)/([^/]+)#', $url, $matches ) ) {
|
||||||
|
return [
|
||||||
|
'platform' => 'gitea',
|
||||||
|
'owner' => $matches[1],
|
||||||
|
'repo' => rtrim( $matches[2], '.git' ),
|
||||||
|
'base_url' => 'https://git.wenpai.org',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用 Gitea
|
||||||
|
if ( preg_match( '#(https?://[^/]+)/([^/]+)/([^/]+)#', $url, $matches ) ) {
|
||||||
|
return [
|
||||||
|
'platform' => 'gitea',
|
||||||
|
'owner' => $matches[2],
|
||||||
|
'repo' => rtrim( $matches[3], '.git' ),
|
||||||
|
'base_url' => $matches[1],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 GitHub 获取 Releases
|
||||||
|
*
|
||||||
|
* @param string $owner 仓库所有者
|
||||||
|
* @param string $repo 仓库名
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function fetch_github_releases( string $owner, string $repo ): array {
|
||||||
|
$api_url = "https://api.github.com/repos/{$owner}/{$repo}/releases";
|
||||||
|
$response = wp_remote_get( $api_url, [
|
||||||
|
'timeout' => 15,
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/vnd.github.v3+json',
|
||||||
|
'User-Agent' => 'WPBridge/' . WPBRIDGE_VERSION,
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return $this->error_response( $response->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$status_code = wp_remote_retrieve_response_code( $response );
|
||||||
|
if ( $status_code !== 200 ) {
|
||||||
|
return $this->error_response( sprintf( __( 'GitHub API 返回错误: %d', 'wpbridge' ), $status_code ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
$releases = json_decode( $body, true );
|
||||||
|
|
||||||
|
if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $releases ) ) {
|
||||||
|
return $this->error_response( __( 'JSON 解析失败', 'wpbridge' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->format_git_releases( $releases, 'GitHub', "{$owner}/{$repo}" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Gitea 获取 Releases
|
||||||
|
*
|
||||||
|
* @param string $base_url API 基础 URL
|
||||||
|
* @param string $owner 仓库所有者
|
||||||
|
* @param string $repo 仓库名
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function fetch_gitea_releases( string $base_url, string $owner, string $repo ): array {
|
||||||
|
$api_url = "{$base_url}/api/v1/repos/{$owner}/{$repo}/releases";
|
||||||
|
$response = wp_remote_get( $api_url, [
|
||||||
|
'timeout' => 15,
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return $this->error_response( $response->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$status_code = wp_remote_retrieve_response_code( $response );
|
||||||
|
if ( $status_code !== 200 ) {
|
||||||
|
return $this->error_response( sprintf( __( 'Gitea API 返回错误: %d', 'wpbridge' ), $status_code ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
$releases = json_decode( $body, true );
|
||||||
|
|
||||||
|
if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $releases ) ) {
|
||||||
|
return $this->error_response( __( 'JSON 解析失败', 'wpbridge' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$source_name = strpos( $base_url, 'wenpai' ) !== false ? '菲码源库' : 'Gitea';
|
||||||
|
return $this->format_git_releases( $releases, $source_name, "{$owner}/{$repo}" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化 Git Releases
|
||||||
|
*
|
||||||
|
* @param array $releases Releases 数据
|
||||||
|
* @param string $source 来源名称
|
||||||
|
* @param string $repo 仓库标识
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function format_git_releases( array $releases, string $source, string $repo ): array {
|
||||||
|
$changelog_html = '<div class="wpbridge-changelog-releases">';
|
||||||
|
$versions = [];
|
||||||
|
|
||||||
|
foreach ( array_slice( $releases, 0, 10 ) as $release ) {
|
||||||
|
$tag = $release['tag_name'] ?? '';
|
||||||
|
$name = $release['name'] ?? $tag;
|
||||||
|
$body = $release['body'] ?? '';
|
||||||
|
$date = $release['published_at'] ?? $release['created_at'] ?? '';
|
||||||
|
$prerelease = $release['prerelease'] ?? false;
|
||||||
|
|
||||||
|
$versions[] = $tag;
|
||||||
|
|
||||||
|
$changelog_html .= '<div class="wpbridge-release-item">';
|
||||||
|
$changelog_html .= '<h4 class="wpbridge-release-title">';
|
||||||
|
$changelog_html .= esc_html( $name );
|
||||||
|
if ( $prerelease ) {
|
||||||
|
$changelog_html .= ' <span class="wpbridge-badge wpbridge-badge-warning">' . esc_html__( '预发布', 'wpbridge' ) . '</span>';
|
||||||
|
}
|
||||||
|
$changelog_html .= '</h4>';
|
||||||
|
|
||||||
|
if ( $date ) {
|
||||||
|
$formatted_date = wp_date( get_option( 'date_format' ), strtotime( $date ) );
|
||||||
|
$changelog_html .= '<p class="wpbridge-release-date">' . esc_html( $formatted_date ) . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $body ) {
|
||||||
|
$body_html = $this->markdown_to_html( $body );
|
||||||
|
$changelog_html .= '<div class="wpbridge-release-body">' . wp_kses_post( $body_html ) . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$changelog_html .= '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$changelog_html .= '</div>';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'source' => $source,
|
||||||
|
'name' => $repo,
|
||||||
|
'slug' => $repo,
|
||||||
|
'version' => $versions[0] ?? '',
|
||||||
|
'last_updated' => $releases[0]['published_at'] ?? '',
|
||||||
|
'changelog_html' => $changelog_html,
|
||||||
|
'versions' => $versions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从自定义源获取更新日志
|
||||||
|
*
|
||||||
|
* @param string $url 源 URL
|
||||||
|
* @param string $type 类型
|
||||||
|
* @param string $slug Slug
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function fetch_custom_changelog( string $url, string $type, string $slug ): array {
|
||||||
|
if ( empty( $url ) ) {
|
||||||
|
return $this->error_response( __( '未配置更新源 URL', 'wpbridge' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_get( $url, [
|
||||||
|
'timeout' => 15,
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return $this->error_response( $response->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
$data = json_decode( $body, true );
|
||||||
|
|
||||||
|
if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) {
|
||||||
|
return $this->error_response( __( '无法解析更新源数据', 'wpbridge' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$changelog_html = '';
|
||||||
|
$version = '';
|
||||||
|
$versions = [];
|
||||||
|
|
||||||
|
if ( isset( $data['sections']['changelog'] ) ) {
|
||||||
|
$changelog_html = $data['sections']['changelog'];
|
||||||
|
$version = $data['version'] ?? '';
|
||||||
|
} elseif ( isset( $data['changelog'] ) ) {
|
||||||
|
$changelog_html = is_array( $data['changelog'] )
|
||||||
|
? $this->format_changelog_array( $data['changelog'] )
|
||||||
|
: $data['changelog'];
|
||||||
|
$version = $data['version'] ?? '';
|
||||||
|
} elseif ( isset( $data['releases'] ) && is_array( $data['releases'] ) ) {
|
||||||
|
return $this->format_git_releases( $data['releases'], __( '自定义源', 'wpbridge' ), $slug );
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'source' => __( '自定义源', 'wpbridge' ),
|
||||||
|
'name' => $data['name'] ?? $slug,
|
||||||
|
'slug' => $slug,
|
||||||
|
'version' => $version,
|
||||||
|
'last_updated' => $data['last_updated'] ?? '',
|
||||||
|
'changelog_html' => $changelog_html,
|
||||||
|
'versions' => $versions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化 changelog 数组
|
||||||
|
*
|
||||||
|
* @param array $changelog Changelog 数组
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function format_changelog_array( array $changelog ): string {
|
||||||
|
$html = '<ul class="wpbridge-changelog-list">';
|
||||||
|
foreach ( $changelog as $version => $changes ) {
|
||||||
|
$html .= '<li>';
|
||||||
|
$html .= '<strong>' . esc_html( $version ) . '</strong>';
|
||||||
|
if ( is_array( $changes ) ) {
|
||||||
|
$html .= '<ul>';
|
||||||
|
foreach ( $changes as $change ) {
|
||||||
|
$html .= '<li>' . esc_html( $change ) . '</li>';
|
||||||
|
}
|
||||||
|
$html .= '</ul>';
|
||||||
|
} else {
|
||||||
|
$html .= '<p>' . esc_html( $changes ) . '</p>';
|
||||||
|
}
|
||||||
|
$html .= '</li>';
|
||||||
|
}
|
||||||
|
$html .= '</ul>';
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单的 Markdown 转 HTML
|
||||||
|
*
|
||||||
|
* @param string $markdown Markdown 文本
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function markdown_to_html( string $markdown ): string {
|
||||||
|
$html = esc_html( $markdown );
|
||||||
|
|
||||||
|
$html = preg_replace( '/^### (.+)$/m', '<h5>$1</h5>', $html );
|
||||||
|
$html = preg_replace( '/^## (.+)$/m', '<h4>$1</h4>', $html );
|
||||||
|
$html = preg_replace( '/^# (.+)$/m', '<h3>$1</h3>', $html );
|
||||||
|
|
||||||
|
$html = preg_replace( '/\*\*(.+?)\*\*/', '<strong>$1</strong>', $html );
|
||||||
|
$html = preg_replace( '/\*(.+?)\*/', '<em>$1</em>', $html );
|
||||||
|
|
||||||
|
$html = preg_replace( '/^[\-\*] (.+)$/m', '<li>$1</li>', $html );
|
||||||
|
$html = preg_replace( '/(<li>.*<\/li>\n?)+/', '<ul>$0</ul>', $html );
|
||||||
|
|
||||||
|
$html = preg_replace( '/`([^`]+)`/', '<code>$1</code>', $html );
|
||||||
|
|
||||||
|
$html = nl2br( $html );
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回错误响应
|
||||||
|
*
|
||||||
|
* @param string $message 错误消息
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function error_response( string $message ): array {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => $message,
|
||||||
|
'source' => '',
|
||||||
|
'name' => '',
|
||||||
|
'slug' => '',
|
||||||
|
'version' => '',
|
||||||
|
'last_updated' => '',
|
||||||
|
'changelog_html' => '',
|
||||||
|
'versions' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存
|
||||||
|
*
|
||||||
|
* @param string $type 类型(plugin/theme)
|
||||||
|
* @param string $slug Slug
|
||||||
|
*/
|
||||||
|
public function clear_cache( string $type, string $slug ): void {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$like = $wpdb->esc_like( '_transient_' . self::CACHE_PREFIX . $type . '_' ) . '%';
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
$like
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有缓存
|
||||||
|
*/
|
||||||
|
public function clear_all_cache(): void {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$like = $wpdb->esc_like( '_transient_' . self::CACHE_PREFIX ) . '%';
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
$like
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
746
includes/Core/CommercialDetector.php
Normal file
746
includes/Core/CommercialDetector.php
Normal file
|
|
@ -0,0 +1,746 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 商业插件检测器
|
||||||
|
*
|
||||||
|
* 自动检测插件是否为商业插件,支持:
|
||||||
|
* - P1: 远程配置 + 已知商业插件列表
|
||||||
|
* - P2: 用户手动标记(优先级最高)
|
||||||
|
* - P3: 智能检测(代码分析)
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.7.5
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CommercialDetector 类
|
||||||
|
*/
|
||||||
|
class CommercialDetector {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件类型常量
|
||||||
|
*/
|
||||||
|
const TYPE_FREE = 'free';
|
||||||
|
const TYPE_COMMERCIAL = 'commercial';
|
||||||
|
const TYPE_PRIVATE = 'private';
|
||||||
|
const TYPE_UNKNOWN = 'unknown';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存选项名(永久存储)
|
||||||
|
*/
|
||||||
|
const CACHE_OPTION = 'wpbridge_plugin_type_cache';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程配置版本选项名
|
||||||
|
*/
|
||||||
|
const CONFIG_VERSION_OPTION = 'wpbridge_remote_config_version';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单例实例
|
||||||
|
*
|
||||||
|
* @var CommercialDetector|null
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户标记缓存
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $user_marks = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测结果缓存(永久存储)
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $detection_cache = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程配置实例
|
||||||
|
*
|
||||||
|
* @var RemoteConfig|null
|
||||||
|
*/
|
||||||
|
private $remote_config = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单例实例
|
||||||
|
*
|
||||||
|
* @return CommercialDetector
|
||||||
|
*/
|
||||||
|
public static function get_instance() {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
$this->load_user_marks();
|
||||||
|
$this->remote_config = RemoteConfig::get_instance();
|
||||||
|
$this->load_detection_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载用户手动标记
|
||||||
|
*/
|
||||||
|
private function load_user_marks() {
|
||||||
|
$this->user_marks = get_option( 'wpbridge_plugin_types', array() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载检测结果缓存(永久存储)
|
||||||
|
* 如果远程配置版本变化,自动清除缓存
|
||||||
|
*/
|
||||||
|
private function load_detection_cache() {
|
||||||
|
$cached = get_option( self::CACHE_OPTION, array() );
|
||||||
|
$this->detection_cache = is_array( $cached ) ? $cached : array();
|
||||||
|
|
||||||
|
// 检查远程配置版本是否变化
|
||||||
|
$current_version = $this->remote_config->get_version();
|
||||||
|
$cached_version = get_option( self::CONFIG_VERSION_OPTION, '' );
|
||||||
|
|
||||||
|
if ( $current_version !== $cached_version ) {
|
||||||
|
// 远程配置更新了,清除缓存
|
||||||
|
$this->detection_cache = array();
|
||||||
|
update_option( self::CONFIG_VERSION_OPTION, $current_version );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存检测结果缓存(永久存储)
|
||||||
|
*/
|
||||||
|
private function save_detection_cache() {
|
||||||
|
update_option( self::CACHE_OPTION, $this->detection_cache, false );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测插件类型
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param string $plugin_file 插件文件路径(可选)
|
||||||
|
* @param bool $skip_api 是否跳过 API 检查(默认 true)
|
||||||
|
* @param bool $use_cache 是否使用缓存(默认 true)
|
||||||
|
* @return array 包含 type 和 source 的数组
|
||||||
|
*/
|
||||||
|
public function detect( $plugin_slug, $plugin_file = '', $skip_api = true, $use_cache = true ) {
|
||||||
|
if ( empty( $plugin_slug ) ) {
|
||||||
|
return array(
|
||||||
|
'type' => self::TYPE_UNKNOWN,
|
||||||
|
'source' => 'none',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// P2: 用户手动标记优先(不缓存,实时读取)
|
||||||
|
if ( isset( $this->user_marks[ $plugin_slug ] ) ) {
|
||||||
|
return array(
|
||||||
|
'type' => $this->user_marks[ $plugin_slug ],
|
||||||
|
'source' => 'manual',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
if ( $use_cache && isset( $this->detection_cache[ $plugin_slug ] ) ) {
|
||||||
|
return $this->detection_cache[ $plugin_slug ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行检测
|
||||||
|
$result = $this->do_detect( $plugin_slug, $plugin_file, $skip_api );
|
||||||
|
|
||||||
|
// 保存到缓存
|
||||||
|
$this->detection_cache[ $plugin_slug ] = $result;
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行实际检测逻辑
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param string $plugin_file 插件文件路径
|
||||||
|
* @param bool $skip_api 是否跳过 API 检查
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function do_detect( $plugin_slug, $plugin_file, $skip_api ) {
|
||||||
|
// P1: 远程配置的商业插件列表
|
||||||
|
$commercial_plugins = $this->remote_config->get_commercial_plugins();
|
||||||
|
if ( in_array( $plugin_slug, $commercial_plugins, true ) ) {
|
||||||
|
return array(
|
||||||
|
'type' => self::TYPE_COMMERCIAL,
|
||||||
|
'source' => 'remote_list',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查插件名称模式
|
||||||
|
if ( $this->has_commercial_pattern( $plugin_slug ) ) {
|
||||||
|
return array(
|
||||||
|
'type' => self::TYPE_COMMERCIAL,
|
||||||
|
'source' => 'pattern',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WordPress.org API 检查(可选)
|
||||||
|
if ( ! $skip_api ) {
|
||||||
|
$wporg_result = $this->check_wordpress_org( $plugin_slug );
|
||||||
|
if ( $wporg_result !== null ) {
|
||||||
|
return array(
|
||||||
|
'type' => $wporg_result ? self::TYPE_FREE : self::TYPE_UNKNOWN,
|
||||||
|
'source' => 'wporg_api',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'type' => self::TYPE_UNKNOWN,
|
||||||
|
'source' => 'none',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P3: 深度扫描检测(智能检测)
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param string $plugin_file 插件文件路径
|
||||||
|
* @return array 包含 type, source, score, reasons 的数组
|
||||||
|
*/
|
||||||
|
public function deep_scan( $plugin_slug, $plugin_file ) {
|
||||||
|
$score = 0;
|
||||||
|
$reasons = array();
|
||||||
|
|
||||||
|
if ( empty( $plugin_file ) ) {
|
||||||
|
return array(
|
||||||
|
'type' => self::TYPE_UNKNOWN,
|
||||||
|
'source' => 'deep_scan',
|
||||||
|
'score' => 0,
|
||||||
|
'reasons' => array( 'no_file' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_dir = WP_PLUGIN_DIR . '/' . dirname( $plugin_file );
|
||||||
|
|
||||||
|
// 1. 检测 License 关键词
|
||||||
|
$license_result = $this->check_license_code( $plugin_dir );
|
||||||
|
if ( $license_result > 0 ) {
|
||||||
|
$score += $license_result;
|
||||||
|
$reasons[] = 'license_code';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检测商业框架
|
||||||
|
$framework_result = $this->check_commercial_frameworks( $plugin_dir );
|
||||||
|
if ( $framework_result > 0 ) {
|
||||||
|
$score += $framework_result;
|
||||||
|
$reasons[] = 'commercial_framework';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检测插件头部信息
|
||||||
|
$header_result = $this->check_plugin_headers( $plugin_file );
|
||||||
|
if ( $header_result > 0 ) {
|
||||||
|
$score += $header_result;
|
||||||
|
$reasons[] = 'commercial_domain';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检测自定义更新机制
|
||||||
|
$updater_result = $this->check_custom_updater( $plugin_dir );
|
||||||
|
if ( $updater_result > 0 ) {
|
||||||
|
$score += $updater_result;
|
||||||
|
$reasons[] = 'custom_updater';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分数 >= 2 判定为商业插件
|
||||||
|
$type = $score >= 2 ? self::TYPE_COMMERCIAL : self::TYPE_UNKNOWN;
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'type' => $type,
|
||||||
|
'source' => 'deep_scan',
|
||||||
|
'score' => $score,
|
||||||
|
'reasons' => $reasons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测 License 相关代码
|
||||||
|
*
|
||||||
|
* @param string $plugin_dir 插件目录
|
||||||
|
* @return int 分数
|
||||||
|
*/
|
||||||
|
private function check_license_code( $plugin_dir ) {
|
||||||
|
$keywords = $this->remote_config->get_license_keywords();
|
||||||
|
if ( empty( $keywords ) ) {
|
||||||
|
$keywords = array(
|
||||||
|
'license_key',
|
||||||
|
'license_status',
|
||||||
|
'activate_license',
|
||||||
|
'deactivate_license',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $this->get_php_files( $plugin_dir, 2 );
|
||||||
|
|
||||||
|
foreach ( $files as $file ) {
|
||||||
|
$content = @file_get_contents( $file );
|
||||||
|
if ( $content === false ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $keywords as $keyword ) {
|
||||||
|
if ( stripos( $content, $keyword ) !== false ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测商业插件框架
|
||||||
|
*
|
||||||
|
* @param string $plugin_dir 插件目录
|
||||||
|
* @return int 分数
|
||||||
|
*/
|
||||||
|
private function check_commercial_frameworks( $plugin_dir ) {
|
||||||
|
$frameworks = $this->remote_config->get_commercial_frameworks();
|
||||||
|
if ( empty( $frameworks ) ) {
|
||||||
|
$frameworks = array(
|
||||||
|
'EDD_SL_Plugin_Updater',
|
||||||
|
'Freemius',
|
||||||
|
'WC_AM_Client',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $this->get_php_files( $plugin_dir, 2 );
|
||||||
|
|
||||||
|
foreach ( $files as $file ) {
|
||||||
|
$content = @file_get_contents( $file );
|
||||||
|
if ( $content === false ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $frameworks as $framework ) {
|
||||||
|
if ( strpos( $content, $framework ) !== false ) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测插件头部信息
|
||||||
|
*
|
||||||
|
* @param string $plugin_file 插件文件
|
||||||
|
* @return int 分数
|
||||||
|
*/
|
||||||
|
private function check_plugin_headers( $plugin_file ) {
|
||||||
|
if ( ! function_exists( 'get_plugin_data' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
|
||||||
|
if ( ! file_exists( $plugin_path ) ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = get_plugin_data( $plugin_path, false, false );
|
||||||
|
$score = 0;
|
||||||
|
|
||||||
|
// 检查 Plugin URI 是否为商业域名
|
||||||
|
$commercial_domains = $this->remote_config->get_commercial_domains();
|
||||||
|
if ( ! empty( $headers['PluginURI'] ) && ! empty( $commercial_domains ) ) {
|
||||||
|
foreach ( $commercial_domains as $domain ) {
|
||||||
|
if ( strpos( $headers['PluginURI'], $domain ) !== false ) {
|
||||||
|
$score += 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查名称是否包含 Pro/Premium
|
||||||
|
if ( ! empty( $headers['Name'] ) ) {
|
||||||
|
if ( preg_match( '/(pro|premium|elite|business|agency)$/i', $headers['Name'] ) ) {
|
||||||
|
$score += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测自定义更新机制
|
||||||
|
*
|
||||||
|
* @param string $plugin_dir 插件目录
|
||||||
|
* @return int 分数
|
||||||
|
*/
|
||||||
|
private function check_custom_updater( $plugin_dir ) {
|
||||||
|
$update_hooks = array(
|
||||||
|
'pre_set_site_transient_update_plugins',
|
||||||
|
'plugins_api',
|
||||||
|
);
|
||||||
|
|
||||||
|
$files = $this->get_php_files( $plugin_dir, 2 );
|
||||||
|
|
||||||
|
foreach ( $files as $file ) {
|
||||||
|
$content = @file_get_contents( $file );
|
||||||
|
if ( $content === false ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $update_hooks as $hook ) {
|
||||||
|
if ( strpos( $content, $hook ) !== false ) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取目录下的 PHP 文件
|
||||||
|
*
|
||||||
|
* @param string $dir 目录
|
||||||
|
* @param int $depth 深度
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function get_php_files( $dir, $depth = 1 ) {
|
||||||
|
$files = array();
|
||||||
|
|
||||||
|
if ( ! is_dir( $dir ) ) {
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主目录 PHP 文件
|
||||||
|
$main_files = glob( $dir . '/*.php' );
|
||||||
|
if ( $main_files ) {
|
||||||
|
$files = array_merge( $files, $main_files );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子目录(如果深度允许)
|
||||||
|
if ( $depth > 1 ) {
|
||||||
|
$subdirs = array( 'includes', 'inc', 'src', 'lib', 'admin' );
|
||||||
|
foreach ( $subdirs as $subdir ) {
|
||||||
|
$subdir_path = $dir . '/' . $subdir;
|
||||||
|
if ( is_dir( $subdir_path ) ) {
|
||||||
|
$sub_files = glob( $subdir_path . '/*.php' );
|
||||||
|
if ( $sub_files ) {
|
||||||
|
$files = array_merge( $files, $sub_files );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制文件数量,避免性能问题
|
||||||
|
return array_slice( $files, 0, 20 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查插件名称是否有商业模式
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function has_commercial_pattern( $plugin_slug ) {
|
||||||
|
$patterns = array(
|
||||||
|
'-pro$',
|
||||||
|
'-premium$',
|
||||||
|
'-elite$',
|
||||||
|
'-business$',
|
||||||
|
'-agency$',
|
||||||
|
'-developer$',
|
||||||
|
'-enterprise$',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $patterns as $pattern ) {
|
||||||
|
if ( preg_match( '/' . $pattern . '/i', $plugin_slug ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查插件是否在 WordPress.org 上存在
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return bool|null
|
||||||
|
*/
|
||||||
|
private function check_wordpress_org( $plugin_slug ) {
|
||||||
|
$cache_key = 'wpbridge_wporg_' . md5( $plugin_slug );
|
||||||
|
$cached = get_transient( $cache_key );
|
||||||
|
|
||||||
|
if ( $cached !== false ) {
|
||||||
|
return $cached === 'yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=' . urlencode( $plugin_slug );
|
||||||
|
$response = wp_remote_get( $url, array( 'timeout' => 5 ) );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code( $response );
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
|
||||||
|
if ( $code === 200 && ! empty( $body ) ) {
|
||||||
|
$data = json_decode( $body, true );
|
||||||
|
if ( isset( $data['slug'] ) ) {
|
||||||
|
set_transient( $cache_key, 'yes', DAY_IN_SECONDS );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_transient( $cache_key, 'no', DAY_IN_SECONDS );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户手动标记
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @param string $type 插件类型
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function set_user_mark( $plugin_slug, $type ) {
|
||||||
|
$valid_types = array(
|
||||||
|
self::TYPE_FREE,
|
||||||
|
self::TYPE_COMMERCIAL,
|
||||||
|
self::TYPE_PRIVATE,
|
||||||
|
self::TYPE_UNKNOWN,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! in_array( $type, $valid_types, true ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $type === self::TYPE_UNKNOWN ) {
|
||||||
|
unset( $this->user_marks[ $plugin_slug ] );
|
||||||
|
} else {
|
||||||
|
$this->user_marks[ $plugin_slug ] = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return update_option( 'wpbridge_plugin_types', $this->user_marks );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户手动标记
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function get_user_mark( $plugin_slug ) {
|
||||||
|
return isset( $this->user_marks[ $plugin_slug ] ) ? $this->user_marks[ $plugin_slug ] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除用户手动标记
|
||||||
|
*
|
||||||
|
* @param string $plugin_slug 插件 slug
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function clear_user_mark( $plugin_slug ) {
|
||||||
|
return $this->set_user_mark( $plugin_slug, self::TYPE_UNKNOWN );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取类型标签
|
||||||
|
*
|
||||||
|
* @param string $type 插件类型
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function get_type_label( $type ) {
|
||||||
|
$labels = array(
|
||||||
|
self::TYPE_FREE => array(
|
||||||
|
'label' => __( '免费', 'wpbridge' ),
|
||||||
|
'color' => 'success',
|
||||||
|
'icon' => 'dashicons-wordpress',
|
||||||
|
),
|
||||||
|
self::TYPE_COMMERCIAL => array(
|
||||||
|
'label' => __( '商业', 'wpbridge' ),
|
||||||
|
'color' => 'warning',
|
||||||
|
'icon' => 'dashicons-awards',
|
||||||
|
),
|
||||||
|
self::TYPE_PRIVATE => array(
|
||||||
|
'label' => __( '私有', 'wpbridge' ),
|
||||||
|
'color' => 'info',
|
||||||
|
'icon' => 'dashicons-lock',
|
||||||
|
),
|
||||||
|
self::TYPE_UNKNOWN => array(
|
||||||
|
'label' => __( '第三方', 'wpbridge' ),
|
||||||
|
'color' => 'gray',
|
||||||
|
'icon' => 'dashicons-admin-plugins',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return isset( $labels[ $type ] ) ? $labels[ $type ] : $labels[ self::TYPE_UNKNOWN ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取远程配置实例
|
||||||
|
*
|
||||||
|
* @return RemoteConfig
|
||||||
|
*/
|
||||||
|
public function get_remote_config() {
|
||||||
|
return $this->remote_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量检测插件类型
|
||||||
|
*
|
||||||
|
* @param array $plugins 插件列表
|
||||||
|
* @param bool $use_cache 是否使用缓存
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function detect_batch( $plugins, $use_cache = true ) {
|
||||||
|
$results = array();
|
||||||
|
foreach ( $plugins as $slug => $file ) {
|
||||||
|
$results[ $slug ] = $this->detect( $slug, $file, true, $use_cache );
|
||||||
|
}
|
||||||
|
// 批量检测后保存缓存
|
||||||
|
$this->save_detection_cache();
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除检测缓存
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function clear_cache() {
|
||||||
|
$this->detection_cache = array();
|
||||||
|
delete_option( self::CONFIG_VERSION_OPTION );
|
||||||
|
return delete_option( self::CACHE_OPTION );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新检测所有插件(同步方式,已废弃)
|
||||||
|
*
|
||||||
|
* @deprecated 使用 prepare_refresh() + refresh_batch() 代替
|
||||||
|
* @return array 检测结果
|
||||||
|
*/
|
||||||
|
public function refresh_all() {
|
||||||
|
// 清除缓存
|
||||||
|
$this->clear_cache();
|
||||||
|
|
||||||
|
// 刷新远程配置
|
||||||
|
$this->remote_config->refresh();
|
||||||
|
|
||||||
|
// 更新配置版本
|
||||||
|
update_option( self::CONFIG_VERSION_OPTION, $this->remote_config->get_version() );
|
||||||
|
|
||||||
|
// 获取所有已安装插件
|
||||||
|
if ( ! function_exists( 'get_plugins' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
$all_plugins = get_plugins();
|
||||||
|
|
||||||
|
// 重新检测
|
||||||
|
$results = array();
|
||||||
|
foreach ( $all_plugins as $plugin_file => $plugin_data ) {
|
||||||
|
$plugin_slug = dirname( $plugin_file );
|
||||||
|
if ( $plugin_slug === '.' ) {
|
||||||
|
$plugin_slug = basename( $plugin_file, '.php' );
|
||||||
|
}
|
||||||
|
$results[ $plugin_slug ] = $this->detect( $plugin_slug, $plugin_file, false, false );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存缓存
|
||||||
|
$this->save_detection_cache();
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备刷新检测(清除缓存,返回插件列表)
|
||||||
|
*
|
||||||
|
* @return array 包含 plugins 列表和 total 数量
|
||||||
|
*/
|
||||||
|
public function prepare_refresh() {
|
||||||
|
// 清除缓存
|
||||||
|
$this->clear_cache();
|
||||||
|
|
||||||
|
// 刷新远程配置
|
||||||
|
$this->remote_config->refresh();
|
||||||
|
|
||||||
|
// 更新配置版本
|
||||||
|
update_option( self::CONFIG_VERSION_OPTION, $this->remote_config->get_version() );
|
||||||
|
|
||||||
|
// 获取所有已安装插件
|
||||||
|
if ( ! function_exists( 'get_plugins' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
$all_plugins = get_plugins();
|
||||||
|
|
||||||
|
// 构建插件列表
|
||||||
|
$plugins = array();
|
||||||
|
foreach ( $all_plugins as $plugin_file => $plugin_data ) {
|
||||||
|
$plugin_slug = dirname( $plugin_file );
|
||||||
|
if ( $plugin_slug === '.' ) {
|
||||||
|
$plugin_slug = basename( $plugin_file, '.php' );
|
||||||
|
}
|
||||||
|
$plugins[] = array(
|
||||||
|
'slug' => $plugin_slug,
|
||||||
|
'file' => $plugin_file,
|
||||||
|
'name' => $plugin_data['Name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'plugins' => $plugins,
|
||||||
|
'total' => count( $plugins ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量检测插件(异步方式)
|
||||||
|
*
|
||||||
|
* @param array $plugins 插件列表,每项包含 slug 和 file
|
||||||
|
* @return array 检测结果
|
||||||
|
*/
|
||||||
|
public function refresh_batch( $plugins ) {
|
||||||
|
$results = array();
|
||||||
|
|
||||||
|
foreach ( $plugins as $plugin ) {
|
||||||
|
if ( ! is_array( $plugin ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$slug = isset( $plugin['slug'] ) ? $plugin['slug'] : '';
|
||||||
|
$file = isset( $plugin['file'] ) ? $plugin['file'] : '';
|
||||||
|
|
||||||
|
if ( empty( $slug ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$results[ $slug ] = $this->detect( $slug, $file, false, false );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存缓存
|
||||||
|
$this->save_detection_cache();
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存统计信息
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_cache_stats() {
|
||||||
|
return array(
|
||||||
|
'count' => count( $this->detection_cache ),
|
||||||
|
'storage' => 'wp_options (permanent)',
|
||||||
|
'config_version' => get_option( self::CONFIG_VERSION_OPTION, 'unknown' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
351
includes/Core/ConfigManager.php
Normal file
351
includes/Core/ConfigManager.php
Normal file
|
|
@ -0,0 +1,351 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 配置导入导出管理
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置管理类
|
||||||
|
*/
|
||||||
|
class ConfigManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置版本
|
||||||
|
*/
|
||||||
|
const CONFIG_VERSION = '1.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要导出的选项
|
||||||
|
*
|
||||||
|
* @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 = [
|
||||||
|
'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 );
|
||||||
|
|
||||||
|
if ( null !== $value ) {
|
||||||
|
// 处理敏感信息
|
||||||
|
if ( ! $include_secrets ) {
|
||||||
|
$value = $this->sanitize_secrets( $option_name, $value );
|
||||||
|
}
|
||||||
|
$config['options'][ $option_name ] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入配置
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入选项
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 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 [
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [ __( 'JSON 格式无效', 'wpbridge' ) ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->import( $config, $merge );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证配置格式
|
||||||
|
*
|
||||||
|
* @param array $config 配置数据
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function validate_config( array $config ): array {
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ( empty( $config['version'] ) ) {
|
||||||
|
$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(
|
||||||
|
__( '配置版本 %s 高于当前支持的版本 %s', 'wpbridge' ),
|
||||||
|
$config['version'],
|
||||||
|
self::CONFIG_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新源中的敏感字段
|
||||||
|
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***';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, [] );
|
||||||
|
|
||||||
|
// 如果当前值为空,直接使用新值
|
||||||
|
if ( empty( $current ) ) {
|
||||||
|
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_source_groups' === $option_name ) {
|
||||||
|
return $this->merge_by_id( $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' );
|
||||||
|
|
||||||
|
foreach ( $new as $source ) {
|
||||||
|
if ( empty( $source['id'] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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' );
|
||||||
|
|
||||||
|
foreach ( $new as $item ) {
|
||||||
|
if ( empty( $item['id'] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$index = array_search( $item['id'], $ids, true );
|
||||||
|
|
||||||
|
if ( false !== $index ) {
|
||||||
|
$merged[ $index ] = array_merge( $merged[ $index ], $item );
|
||||||
|
} else {
|
||||||
|
$merged[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建备份
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置为默认配置
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
271
includes/Core/DefaultsManager.php
Normal file
271
includes/Core/DefaultsManager.php
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 默认规则管理
|
||||||
|
*
|
||||||
|
* 方案 B:项目优先架构 - 默认规则层
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.6.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认规则管理类
|
||||||
|
*
|
||||||
|
* 管理全局和类型级别的默认更新源配置
|
||||||
|
*/
|
||||||
|
class DefaultsManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项名称
|
||||||
|
*/
|
||||||
|
const OPTION_NAME = 'wpbridge_defaults';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作用范围
|
||||||
|
*/
|
||||||
|
const SCOPE_GLOBAL = 'global';
|
||||||
|
const SCOPE_PLUGIN = 'plugin';
|
||||||
|
const SCOPE_THEME = 'theme';
|
||||||
|
const SCOPE_CORE = 'core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存的默认规则
|
||||||
|
*
|
||||||
|
* @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, [] );
|
||||||
|
$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 作用范围
|
||||||
|
* @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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认源顺序
|
||||||
|
*
|
||||||
|
* @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, [ '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 = [];
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有配置的源可用,回退到 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否需要签名验证
|
||||||
|
*
|
||||||
|
* @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_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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保默认规则存在
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
'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'] = [ 'wporg' ];
|
||||||
|
$base['trust_floor'] = 90;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::SCOPE_PLUGIN:
|
||||||
|
case self::SCOPE_THEME:
|
||||||
|
$base['source_order'] = [ 'wenpai-mirror', 'wporg' ];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::SCOPE_GLOBAL:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
406
includes/Core/ItemSourceManager.php
Normal file
406
includes/Core/ItemSourceManager.php
Normal file
|
|
@ -0,0 +1,406 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 项目配置管理
|
||||||
|
*
|
||||||
|
* 方案 B:项目优先架构 - 项目配置层
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.6.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目配置管理类
|
||||||
|
*
|
||||||
|
* 管理项目(插件/主题)与更新源的绑定关系
|
||||||
|
*/
|
||||||
|
class ItemSourceManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项名称
|
||||||
|
*/
|
||||||
|
const OPTION_NAME = 'wpbridge_item_sources';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目类型
|
||||||
|
*/
|
||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存的配置
|
||||||
|
*
|
||||||
|
* @var array|null
|
||||||
|
*/
|
||||||
|
private ?array $cached_configs = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源注册表
|
||||||
|
*
|
||||||
|
* @var SourceRegistry
|
||||||
|
*/
|
||||||
|
private SourceRegistry $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, [] );
|
||||||
|
}
|
||||||
|
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 $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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置项目配置
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除项目配置
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置项目的更新源
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加或更新源
|
||||||
|
$source_ids[ $source_key ] = $priority;
|
||||||
|
arsort( $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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source_ids = $config['source_ids'] ?? [];
|
||||||
|
unset( $source_ids[ $source_key ] );
|
||||||
|
|
||||||
|
// 如果没有自定义源了,切回默认模式
|
||||||
|
$mode = empty( $source_ids ) ? self::MODE_DEFAULT : self::MODE_CUSTOM;
|
||||||
|
|
||||||
|
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, [ 'mode' => self::MODE_DISABLED ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用项目更新(切回默认)
|
||||||
|
*
|
||||||
|
* @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, [
|
||||||
|
'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 );
|
||||||
|
|
||||||
|
// 如果禁用更新,返回空
|
||||||
|
if ( $config && ( $config['mode'] ?? '' ) === self::MODE_DISABLED ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有自定义配置,使用自定义源
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按优先级排序
|
||||||
|
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 前缀推断
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 'dropin:' ) === 0 ) {
|
||||||
|
return self::TYPE_DROPIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无前缀时默认为插件类型(向后兼容)
|
||||||
|
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 项目键列表
|
||||||
|
* @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, [
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
includes/Core/Loader.php
Normal file
68
includes/Core/Loader.php
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 自动加载器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PSR-4 风格自动加载器
|
||||||
|
*/
|
||||||
|
class Loader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 命名空间前缀
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private static string $namespace_prefix = 'WPBridge\\';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础目录
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private static string $base_dir = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册自动加载器
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取相对类名
|
||||||
|
$relative_class = substr( $class, $len );
|
||||||
|
|
||||||
|
// 转换为文件路径
|
||||||
|
$file = self::$base_dir . str_replace( '\\', '/', $relative_class ) . '.php';
|
||||||
|
|
||||||
|
// 如果文件存在则加载
|
||||||
|
if ( file_exists( $file ) ) {
|
||||||
|
require_once $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即注册自动加载器
|
||||||
|
Loader::register();
|
||||||
175
includes/Core/Logger.php
Normal file
175
includes/Core/Logger.php
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 日志系统
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志类
|
||||||
|
*/
|
||||||
|
class Logger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志级别
|
||||||
|
*/
|
||||||
|
const LEVEL_DEBUG = 'debug';
|
||||||
|
const LEVEL_INFO = 'info';
|
||||||
|
const LEVEL_WARNING = 'warning';
|
||||||
|
const LEVEL_ERROR = 'error';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项名称
|
||||||
|
*/
|
||||||
|
const OPTION_LOGS = 'wpbridge_logs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大日志条数
|
||||||
|
*/
|
||||||
|
const MAX_LOGS = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings|null
|
||||||
|
*/
|
||||||
|
private static ?Settings $settings = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 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 = [] ): void {
|
||||||
|
self::log( self::LEVEL_DEBUG, $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 = [] ): void {
|
||||||
|
self::log( self::LEVEL_WARNING, $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 = [] ): 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, [] );
|
||||||
|
|
||||||
|
// 添加新日志
|
||||||
|
$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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
update_option( self::OPTION_LOGS, $logs, false );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理上下文数据(脱敏)
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有日志
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间倒序
|
||||||
|
return array_reverse( $logs );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有日志
|
||||||
|
*/
|
||||||
|
public static function clear(): void {
|
||||||
|
delete_option( self::OPTION_LOGS );
|
||||||
|
}
|
||||||
|
}
|
||||||
561
includes/Core/MigrationManager.php
Normal file
561
includes/Core/MigrationManager.php
Normal file
|
|
@ -0,0 +1,561 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 数据迁移管理
|
||||||
|
*
|
||||||
|
* 方案 B:从方案 A 迁移到项目优先架构
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.6.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据迁移管理类
|
||||||
|
*/
|
||||||
|
class MigrationManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移版本选项
|
||||||
|
*/
|
||||||
|
const OPTION_VERSION = 'wpbridge_migration_version';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备份选项前缀
|
||||||
|
*/
|
||||||
|
const BACKUP_PREFIX = 'wpbridge_backup_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前迁移版本
|
||||||
|
*/
|
||||||
|
const CURRENT_VERSION = '0.6.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 旧选项名称(方案 A)
|
||||||
|
*/
|
||||||
|
const OLD_SOURCES = 'wpbridge_sources';
|
||||||
|
const OLD_SETTINGS = 'wpbridge_settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源注册表
|
||||||
|
*
|
||||||
|
* @var SourceRegistry
|
||||||
|
*/
|
||||||
|
private SourceRegistry $source_registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目配置管理器
|
||||||
|
*
|
||||||
|
* @var ItemSourceManager
|
||||||
|
*/
|
||||||
|
private ItemSourceManager $item_manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认规则管理器
|
||||||
|
*
|
||||||
|
* @var DefaultsManager
|
||||||
|
*/
|
||||||
|
private DefaultsManager $defaults_manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移日志
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $log = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源 ID 映射表(旧 ID → 新 key)
|
||||||
|
*
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private array $source_id_map = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param SourceRegistry $source_registry 源注册表
|
||||||
|
* @param ItemSourceManager $item_manager 项目配置管理器
|
||||||
|
* @param DefaultsManager $defaults_manager 默认规则管理器
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
SourceRegistry $source_registry,
|
||||||
|
ItemSourceManager $item_manager,
|
||||||
|
DefaultsManager $defaults_manager
|
||||||
|
) {
|
||||||
|
$this->source_registry = $source_registry;
|
||||||
|
$this->item_manager = $item_manager;
|
||||||
|
$this->defaults_manager = $defaults_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否需要迁移
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function needs_migration(): bool {
|
||||||
|
$current = get_option( self::OPTION_VERSION, '0.0.0' );
|
||||||
|
return version_compare( $current, self::CURRENT_VERSION, '<' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行迁移
|
||||||
|
*
|
||||||
|
* @return array 迁移结果
|
||||||
|
*/
|
||||||
|
public function migrate(): array {
|
||||||
|
$this->log = [];
|
||||||
|
$this->source_id_map = [];
|
||||||
|
$this->log( 'info', '开始迁移到方案 B (v' . self::CURRENT_VERSION . ')' );
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 备份旧数据
|
||||||
|
$this->backup_old_data();
|
||||||
|
|
||||||
|
// 2. 迁移源数据
|
||||||
|
$this->migrate_sources();
|
||||||
|
|
||||||
|
// 3. 迁移项目配置
|
||||||
|
$this->migrate_item_configs();
|
||||||
|
|
||||||
|
// 4. 设置默认规则
|
||||||
|
$this->setup_defaults();
|
||||||
|
|
||||||
|
// 5. 验证迁移
|
||||||
|
$validation = $this->validate_migration();
|
||||||
|
if ( ! $validation['success'] ) {
|
||||||
|
throw new \Exception( '迁移验证失败: ' . implode( ', ', $validation['errors'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 更新版本号
|
||||||
|
update_option( self::OPTION_VERSION, self::CURRENT_VERSION );
|
||||||
|
|
||||||
|
// 7. 清理旧数据(保留备份以便回滚)
|
||||||
|
$this->cleanup_old_data();
|
||||||
|
|
||||||
|
$this->log( 'success', '迁移完成' );
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'log' => $this->log,
|
||||||
|
'source_id_map' => $this->source_id_map,
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$this->log( 'error', '迁移失败: ' . $e->getMessage() );
|
||||||
|
|
||||||
|
// 尝试回滚
|
||||||
|
$this->rollback();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'log' => $this->log,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备份旧数据
|
||||||
|
*/
|
||||||
|
private function backup_old_data(): void {
|
||||||
|
$this->log( 'info', '备份旧数据...' );
|
||||||
|
|
||||||
|
$old_sources = get_option( self::OLD_SOURCES, [] );
|
||||||
|
$old_settings = get_option( self::OLD_SETTINGS, [] );
|
||||||
|
|
||||||
|
update_option( self::BACKUP_PREFIX . 'sources', $old_sources, false );
|
||||||
|
update_option( self::BACKUP_PREFIX . 'settings', $old_settings, false );
|
||||||
|
update_option( self::BACKUP_PREFIX . 'timestamp', time(), false );
|
||||||
|
|
||||||
|
$this->log( 'info', '已备份 ' . count( $old_sources ) . ' 个源配置' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移源数据
|
||||||
|
*/
|
||||||
|
private function migrate_sources(): void {
|
||||||
|
$this->log( 'info', '迁移源数据...' );
|
||||||
|
|
||||||
|
$old_sources = get_option( self::OLD_SOURCES, [] );
|
||||||
|
$migrated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ( $old_sources as $old_source ) {
|
||||||
|
$source_key = $this->convert_source( $old_source );
|
||||||
|
if ( $source_key ) {
|
||||||
|
$migrated++;
|
||||||
|
} else {
|
||||||
|
$skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log( 'info', "源迁移完成: {$migrated} 成功, {$skipped} 跳过" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换单个源
|
||||||
|
*
|
||||||
|
* @param array $old_source 旧源数据
|
||||||
|
* @return string|false 成功返回 source_key
|
||||||
|
*/
|
||||||
|
private function convert_source( array $old_source ) {
|
||||||
|
$old_id = $old_source['id'] ?? '';
|
||||||
|
|
||||||
|
// 跳过预置源(新系统会自动创建)
|
||||||
|
if ( ! empty( $old_source['is_preset'] ) ) {
|
||||||
|
// 预置源映射到新的预置源 key
|
||||||
|
$preset_map = [
|
||||||
|
'wporg' => 'wporg',
|
||||||
|
'wenpai' => 'wenpai-mirror',
|
||||||
|
'wenpai-mirror' => 'wenpai-mirror',
|
||||||
|
'fair' => 'fair-aspirecloud',
|
||||||
|
'fair-aspirecloud' => 'fair-aspirecloud',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ( $old_id && isset( $preset_map[ $old_id ] ) ) {
|
||||||
|
$this->source_id_map[ $old_id ] = $preset_map[ $old_id ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射旧类型到新类型
|
||||||
|
$type_map = [
|
||||||
|
'json' => SourceRegistry::TYPE_JSON,
|
||||||
|
'github' => SourceRegistry::TYPE_GIT,
|
||||||
|
'gitlab' => SourceRegistry::TYPE_GIT,
|
||||||
|
'arkpress' => SourceRegistry::TYPE_ARKPRESS,
|
||||||
|
'custom' => SourceRegistry::TYPE_CUSTOM,
|
||||||
|
];
|
||||||
|
|
||||||
|
$new_source = [
|
||||||
|
'source_key' => $old_id,
|
||||||
|
'name' => $old_source['name'] ?? '',
|
||||||
|
'type' => $type_map[ $old_source['type'] ?? 'custom' ] ?? SourceRegistry::TYPE_CUSTOM,
|
||||||
|
'api_url' => $old_source['api_url'] ?? '',
|
||||||
|
'enabled' => $old_source['enabled'] ?? true,
|
||||||
|
'default_priority' => $old_source['priority'] ?? 50,
|
||||||
|
'auth_type' => ! empty( $old_source['auth_token'] ) ? SourceRegistry::AUTH_BEARER : SourceRegistry::AUTH_NONE,
|
||||||
|
'auth_secret_ref' => ! empty( $old_source['auth_token'] ) ? $this->store_secret( $old_source['auth_token'] ) : '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$new_key = $this->source_registry->add( $new_source );
|
||||||
|
|
||||||
|
// 记录映射关系
|
||||||
|
if ( $new_key && $old_id ) {
|
||||||
|
$this->source_id_map[ $old_id ] = $new_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移项目配置
|
||||||
|
*/
|
||||||
|
private function migrate_item_configs(): void {
|
||||||
|
$this->log( 'info', '迁移项目配置...' );
|
||||||
|
|
||||||
|
$old_sources = get_option( self::OLD_SOURCES, [] );
|
||||||
|
$migrated = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ( $old_sources as $old_source ) {
|
||||||
|
// 跳过通配符配置(将作为默认规则处理)
|
||||||
|
if ( empty( $old_source['slug'] ) || $old_source['slug'] === '*' ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_type = $old_source['item_type'] ?? 'plugin';
|
||||||
|
$item_key = $this->resolve_item_key( $old_source['slug'], $item_type );
|
||||||
|
|
||||||
|
if ( ! $item_key ) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查源 ID 是否有效
|
||||||
|
$old_source_id = $old_source['id'] ?? '';
|
||||||
|
if ( empty( $old_source_id ) ) {
|
||||||
|
$this->log( 'warning', "跳过项目 {$item_key}: 源 ID 为空" );
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用映射后的源 key
|
||||||
|
$new_source_key = $this->source_id_map[ $old_source_id ] ?? $old_source_id;
|
||||||
|
|
||||||
|
// 验证新源存在
|
||||||
|
if ( ! $this->source_registry->get( $new_source_key ) ) {
|
||||||
|
$this->log( 'warning', "跳过项目 {$item_key}: 源 {$new_source_key} 不存在" );
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->item_manager->set( $item_key, [
|
||||||
|
'item_type' => $item_type,
|
||||||
|
'item_slug' => $old_source['slug'],
|
||||||
|
'mode' => ItemSourceManager::MODE_CUSTOM,
|
||||||
|
'source_ids' => [ $new_source_key => $old_source['priority'] ?? 50 ],
|
||||||
|
] );
|
||||||
|
$migrated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log( 'info', "项目配置迁移完成: {$migrated} 成功, {$skipped} 跳过" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析项目键
|
||||||
|
*
|
||||||
|
* @param string $slug 项目 slug
|
||||||
|
* @param string $item_type 项目类型
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
private function resolve_item_key( string $slug, string $item_type ): ?string {
|
||||||
|
if ( $item_type === 'plugin' ) {
|
||||||
|
// 尝试查找已安装的插件
|
||||||
|
if ( ! function_exists( 'get_plugins' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugins = get_plugins();
|
||||||
|
foreach ( $plugins as $plugin_file => $plugin_data ) {
|
||||||
|
// 匹配 slug
|
||||||
|
$plugin_slug = dirname( $plugin_file );
|
||||||
|
if ( $plugin_slug === '.' ) {
|
||||||
|
$plugin_slug = basename( $plugin_file, '.php' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $plugin_slug === $slug ) {
|
||||||
|
return 'plugin:' . $plugin_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未找到已安装插件,使用 slug 作为预配置
|
||||||
|
return 'plugin:' . $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $item_type === 'theme' ) {
|
||||||
|
// 检查主题是否存在
|
||||||
|
$theme = wp_get_theme( $slug );
|
||||||
|
if ( $theme->exists() ) {
|
||||||
|
return 'theme:' . $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预配置
|
||||||
|
return 'theme:' . $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置默认规则
|
||||||
|
*/
|
||||||
|
private function setup_defaults(): void {
|
||||||
|
$this->log( 'info', '设置默认规则...' );
|
||||||
|
|
||||||
|
$old_sources = get_option( self::OLD_SOURCES, [] );
|
||||||
|
|
||||||
|
// 查找通配符配置
|
||||||
|
$plugin_defaults = [];
|
||||||
|
$theme_defaults = [];
|
||||||
|
|
||||||
|
foreach ( $old_sources as $old_source ) {
|
||||||
|
if ( empty( $old_source['slug'] ) || $old_source['slug'] === '*' ) {
|
||||||
|
$item_type = $old_source['item_type'] ?? 'plugin';
|
||||||
|
$old_source_id = $old_source['id'] ?? '';
|
||||||
|
|
||||||
|
// 使用映射后的源 key
|
||||||
|
$new_source_key = $this->source_id_map[ $old_source_id ] ?? $old_source_id;
|
||||||
|
|
||||||
|
// 验证源存在
|
||||||
|
if ( ! $this->source_registry->get( $new_source_key ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $item_type === 'plugin' && $new_source_key ) {
|
||||||
|
$plugin_defaults[] = $new_source_key;
|
||||||
|
} elseif ( $item_type === 'theme' && $new_source_key ) {
|
||||||
|
$theme_defaults[] = $new_source_key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认源顺序
|
||||||
|
if ( ! empty( $plugin_defaults ) ) {
|
||||||
|
$this->defaults_manager->set_source_order( DefaultsManager::SCOPE_PLUGIN, $plugin_defaults );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $theme_defaults ) ) {
|
||||||
|
$this->defaults_manager->set_source_order( DefaultsManager::SCOPE_THEME, $theme_defaults );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log( 'info', '默认规则设置完成' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证迁移
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function validate_migration(): array {
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// 检查源注册表
|
||||||
|
$sources = $this->source_registry->get_all();
|
||||||
|
if ( empty( $sources ) ) {
|
||||||
|
$errors[] = '源注册表为空';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查预置源
|
||||||
|
$preset_keys = [ 'wporg', 'wenpai-mirror', 'fair-aspirecloud' ];
|
||||||
|
foreach ( $preset_keys as $key ) {
|
||||||
|
if ( ! $this->source_registry->get( $key ) ) {
|
||||||
|
$errors[] = "预置源 {$key} 不存在";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查默认规则
|
||||||
|
$defaults = $this->defaults_manager->get_all();
|
||||||
|
if ( empty( $defaults ) ) {
|
||||||
|
$errors[] = '默认规则为空';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => empty( $errors ),
|
||||||
|
'errors' => $errors,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回滚迁移
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function rollback(): bool {
|
||||||
|
$this->log( 'warning', '开始回滚...' );
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 恢复备份数据
|
||||||
|
$backup_sources = get_option( self::BACKUP_PREFIX . 'sources', [] );
|
||||||
|
$backup_settings = get_option( self::BACKUP_PREFIX . 'settings', [] );
|
||||||
|
|
||||||
|
if ( ! empty( $backup_sources ) ) {
|
||||||
|
update_option( self::OLD_SOURCES, $backup_sources );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $backup_settings ) ) {
|
||||||
|
update_option( self::OLD_SETTINGS, $backup_settings );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除新数据
|
||||||
|
delete_option( SourceRegistry::OPTION_NAME );
|
||||||
|
delete_option( ItemSourceManager::OPTION_NAME );
|
||||||
|
delete_option( DefaultsManager::OPTION_NAME );
|
||||||
|
delete_option( self::OPTION_VERSION );
|
||||||
|
|
||||||
|
$this->log( 'info', '回滚完成' );
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$this->log( 'error', '回滚失败: ' . $e->getMessage() );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理备份数据
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function cleanup_backup(): bool {
|
||||||
|
delete_option( self::BACKUP_PREFIX . 'sources' );
|
||||||
|
delete_option( self::BACKUP_PREFIX . 'settings' );
|
||||||
|
delete_option( self::BACKUP_PREFIX . 'timestamp' );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理旧数据
|
||||||
|
*
|
||||||
|
* 迁移成功后删除旧的选项数据
|
||||||
|
* 注意:备份数据保留以便回滚
|
||||||
|
*/
|
||||||
|
private function cleanup_old_data(): void {
|
||||||
|
$this->log( 'info', '清理旧数据...' );
|
||||||
|
|
||||||
|
// 删除旧的选项
|
||||||
|
delete_option( self::OLD_SOURCES );
|
||||||
|
delete_option( self::OLD_SETTINGS );
|
||||||
|
|
||||||
|
$this->log( 'info', '旧数据清理完成' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完全清理(包括备份)
|
||||||
|
*
|
||||||
|
* 在确认迁移稳定后调用
|
||||||
|
* 注意:旧数据已在 migrate() 中清理,此方法主要清理备份
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function full_cleanup(): bool {
|
||||||
|
$this->cleanup_backup();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储敏感信息
|
||||||
|
*
|
||||||
|
* @param string $secret 敏感信息
|
||||||
|
* @return string 引用键
|
||||||
|
*/
|
||||||
|
private function store_secret( string $secret ): string {
|
||||||
|
$ref = 'secret_' . wp_generate_uuid4();
|
||||||
|
update_option( 'wpbridge_secret_' . $ref, $secret, false );
|
||||||
|
return $ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录日志
|
||||||
|
*
|
||||||
|
* @param string $level 日志级别
|
||||||
|
* @param string $message 日志消息
|
||||||
|
*/
|
||||||
|
private function log( string $level, string $message ): void {
|
||||||
|
$this->log[] = [
|
||||||
|
'level' => $level,
|
||||||
|
'message' => $message,
|
||||||
|
'timestamp' => current_time( 'mysql' ),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 同时写入 WordPress 日志
|
||||||
|
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||||
|
error_log( "[WPBridge Migration] [{$level}] {$message}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取迁移日志
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_log(): array {
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取源 ID 映射表
|
||||||
|
*
|
||||||
|
* @return array<string, string> 旧 ID → 新 key
|
||||||
|
*/
|
||||||
|
public function get_source_id_map(): array {
|
||||||
|
return $this->source_id_map;
|
||||||
|
}
|
||||||
|
}
|
||||||
886
includes/Core/Plugin.php
Normal file
886
includes/Core/Plugin.php
Normal file
|
|
@ -0,0 +1,886 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 插件主类
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
use WPBridge\UpdateSource\PluginUpdater;
|
||||||
|
use WPBridge\UpdateSource\ThemeUpdater;
|
||||||
|
use WPBridge\Admin\AdminPage;
|
||||||
|
use WPBridge\AIBridge\AIGateway;
|
||||||
|
use WPBridge\Commercial\CommercialManager;
|
||||||
|
use WPBridge\Notification\NotificationManager;
|
||||||
|
use WPBridge\SourceGroup\GroupManager;
|
||||||
|
use WPBridge\API\RestController;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件主类(单例模式)
|
||||||
|
*/
|
||||||
|
class Plugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单例实例
|
||||||
|
*
|
||||||
|
* @var Plugin|null
|
||||||
|
*/
|
||||||
|
private static ?Plugin $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置管理器
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源注册表(方案 B)
|
||||||
|
*
|
||||||
|
* @var SourceRegistry|null
|
||||||
|
*/
|
||||||
|
private ?SourceRegistry $source_registry = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目配置管理器(方案 B)
|
||||||
|
*
|
||||||
|
* @var ItemSourceManager|null
|
||||||
|
*/
|
||||||
|
private ?ItemSourceManager $item_manager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认规则管理器(方案 B)
|
||||||
|
*
|
||||||
|
* @var DefaultsManager|null
|
||||||
|
*/
|
||||||
|
private ?DefaultsManager $defaults_manager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件更新器
|
||||||
|
*
|
||||||
|
* @var PluginUpdater|null
|
||||||
|
*/
|
||||||
|
private ?PluginUpdater $plugin_updater = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题更新器
|
||||||
|
*
|
||||||
|
* @var ThemeUpdater|null
|
||||||
|
*/
|
||||||
|
private ?ThemeUpdater $theme_updater = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 网关
|
||||||
|
*
|
||||||
|
* @var AIGateway|null
|
||||||
|
*/
|
||||||
|
private ?AIGateway $ai_gateway = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商业插件管理器
|
||||||
|
*
|
||||||
|
* @var CommercialManager|null
|
||||||
|
*/
|
||||||
|
private ?CommercialManager $commercial_manager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知管理器
|
||||||
|
*
|
||||||
|
* @var NotificationManager|null
|
||||||
|
*/
|
||||||
|
private ?NotificationManager $notification_manager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源分组管理器
|
||||||
|
*
|
||||||
|
* @var GroupManager|null
|
||||||
|
*/
|
||||||
|
private ?GroupManager $group_manager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API 控制器
|
||||||
|
*
|
||||||
|
* @var RestController|null
|
||||||
|
*/
|
||||||
|
private ?RestController $rest_controller = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商业插件检测器
|
||||||
|
*
|
||||||
|
* @var CommercialDetector|null
|
||||||
|
*/
|
||||||
|
private ?CommercialDetector $commercial_detector = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单例实例
|
||||||
|
*
|
||||||
|
* @return Plugin
|
||||||
|
*/
|
||||||
|
public static function get_instance(): Plugin {
|
||||||
|
if ( null === self::$instance ) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私有构造函数
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
$this->settings = new Settings();
|
||||||
|
|
||||||
|
// 初始化方案 B 数据模型
|
||||||
|
$this->source_registry = new SourceRegistry();
|
||||||
|
$this->defaults_manager = new DefaultsManager();
|
||||||
|
$this->item_manager = new ItemSourceManager( $this->source_registry );
|
||||||
|
|
||||||
|
// 检查并执行迁移
|
||||||
|
$this->maybe_migrate();
|
||||||
|
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并执行数据迁移
|
||||||
|
*/
|
||||||
|
private function maybe_migrate(): void {
|
||||||
|
$migration = new MigrationManager(
|
||||||
|
$this->source_registry,
|
||||||
|
$this->item_manager,
|
||||||
|
$this->defaults_manager
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $migration->needs_migration() ) {
|
||||||
|
$result = $migration->migrate();
|
||||||
|
if ( ! $result['success'] ) {
|
||||||
|
// 记录迁移失败
|
||||||
|
Logger::error( '数据迁移失败', [ 'log' => $result['log'] ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks(): void {
|
||||||
|
// 加载文本域
|
||||||
|
add_action( 'init', [ $this, 'load_textdomain' ] );
|
||||||
|
|
||||||
|
// 初始化更新器
|
||||||
|
add_action( 'init', [ $this, 'init_updaters' ] );
|
||||||
|
|
||||||
|
// 管理界面 - 在 plugins_loaded 之后立即初始化
|
||||||
|
if ( is_admin() ) {
|
||||||
|
$this->init_admin();
|
||||||
|
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
|
||||||
|
|
||||||
|
// AJAX 处理
|
||||||
|
add_action( 'wp_ajax_wpbridge_set_plugin_type', [ $this, 'ajax_set_plugin_type' ] );
|
||||||
|
add_action( 'wp_ajax_wpbridge_refresh_commercial_detection', [ $this, 'ajax_refresh_commercial_detection' ] );
|
||||||
|
add_action( 'wp_ajax_wpbridge_prepare_refresh', [ $this, 'ajax_prepare_refresh' ] );
|
||||||
|
add_action( 'wp_ajax_wpbridge_refresh_batch', [ $this, 'ajax_refresh_batch' ] );
|
||||||
|
add_action( 'wp_ajax_wpbridge_export_config', [ $this, 'ajax_export_config' ] );
|
||||||
|
add_action( 'wp_ajax_wpbridge_import_config', [ $this, 'ajax_import_config' ] );
|
||||||
|
add_action( 'wp_ajax_wpbridge_lock_version', [ $this, 'ajax_lock_version' ] );
|
||||||
|
add_action( 'wp_ajax_wpbridge_unlock_version', [ $this, 'ajax_unlock_version' ] );
|
||||||
|
add_action( 'wp_ajax_wpbridge_rollback', [ $this, 'ajax_rollback' ] );
|
||||||
|
add_action( 'wp_ajax_wpbridge_get_backups', [ $this, 'ajax_get_backups' ] );
|
||||||
|
add_action( 'wp_ajax_wpbridge_get_changelog', [ $this, 'ajax_get_changelog' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件链接
|
||||||
|
add_filter( 'plugin_action_links_' . WPBRIDGE_BASENAME, [ $this, 'add_action_links' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载文本域
|
||||||
|
*/
|
||||||
|
public function load_textdomain(): void {
|
||||||
|
load_plugin_textdomain(
|
||||||
|
'wpbridge',
|
||||||
|
false,
|
||||||
|
dirname( WPBRIDGE_BASENAME ) . '/languages'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化更新器
|
||||||
|
*/
|
||||||
|
public function init_updaters(): void {
|
||||||
|
$this->plugin_updater = new PluginUpdater( $this->settings );
|
||||||
|
$this->theme_updater = new ThemeUpdater( $this->settings );
|
||||||
|
$this->ai_gateway = new AIGateway( $this->settings );
|
||||||
|
$this->commercial_manager = new CommercialManager( $this->settings );
|
||||||
|
$this->notification_manager = new NotificationManager( $this->settings );
|
||||||
|
$this->group_manager = new GroupManager( $this->settings );
|
||||||
|
$this->rest_controller = new RestController( $this->settings );
|
||||||
|
$this->commercial_detector = CommercialDetector::get_instance();
|
||||||
|
|
||||||
|
// 初始化版本锁定
|
||||||
|
VersionLock::get_instance();
|
||||||
|
|
||||||
|
// 初始化备份管理器
|
||||||
|
BackupManager::get_instance();
|
||||||
|
|
||||||
|
// 初始化 Site Health 集成
|
||||||
|
new SiteHealth( $this->settings );
|
||||||
|
|
||||||
|
// 注册 AI 适配器
|
||||||
|
$this->register_ai_adapters();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 AI 适配器
|
||||||
|
*/
|
||||||
|
private function register_ai_adapters(): void {
|
||||||
|
if ( null === $this->ai_gateway ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ai_gateway->register_adapter(
|
||||||
|
'yoast',
|
||||||
|
new \WPBridge\AIBridge\Adapters\YoastAdapter( $this->settings )
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->ai_gateway->register_adapter(
|
||||||
|
'rankmath',
|
||||||
|
new \WPBridge\AIBridge\Adapters\RankMathAdapter( $this->settings )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化管理界面
|
||||||
|
*/
|
||||||
|
public function init_admin(): void {
|
||||||
|
new AdminPage( $this->settings );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载管理界面资源
|
||||||
|
*
|
||||||
|
* @param string $hook 当前页面钩子
|
||||||
|
*/
|
||||||
|
public function enqueue_admin_assets( string $hook ): void {
|
||||||
|
// 只在插件页面加载
|
||||||
|
if ( strpos( $hook, 'wpbridge' ) === false ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'wpbridge-admin',
|
||||||
|
WPBRIDGE_URL . 'assets/css/admin.css',
|
||||||
|
[],
|
||||||
|
WPBRIDGE_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'wpbridge-admin',
|
||||||
|
WPBRIDGE_URL . 'assets/js/admin.js',
|
||||||
|
[ 'jquery' ],
|
||||||
|
WPBRIDGE_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_localize_script( 'wpbridge-admin', 'wpbridge', [
|
||||||
|
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||||
|
'nonce' => wp_create_nonce( 'wpbridge_nonce' ),
|
||||||
|
'i18n' => [
|
||||||
|
'confirm_delete' => __( '确定要删除这个更新源吗?', 'wpbridge' ),
|
||||||
|
'confirm_revoke' => __( '确定要撤销此 API Key 吗?', 'wpbridge' ),
|
||||||
|
'confirm_clear_logs' => __( '确定要清除所有日志吗?', 'wpbridge' ),
|
||||||
|
'testing' => __( '测试中...', 'wpbridge' ),
|
||||||
|
'success' => __( '成功', 'wpbridge' ),
|
||||||
|
'failed' => __( '操作失败', 'wpbridge' ),
|
||||||
|
'enabled' => __( '已启用', 'wpbridge' ),
|
||||||
|
'disabled' => __( '已禁用', 'wpbridge' ),
|
||||||
|
'healthy' => __( '正常', 'wpbridge' ),
|
||||||
|
'degraded' => __( '降级', 'wpbridge' ),
|
||||||
|
'failed_status' => __( '失败', 'wpbridge' ),
|
||||||
|
'test_success' => __( '连接成功', 'wpbridge' ),
|
||||||
|
'test_degraded' => __( '连接异常', 'wpbridge' ),
|
||||||
|
'cache_cleared' => __( '缓存已清除', 'wpbridge' ),
|
||||||
|
'logs_cleared' => __( '日志已清除', 'wpbridge' ),
|
||||||
|
'no_logs' => __( '暂无日志记录', 'wpbridge' ),
|
||||||
|
'enter_key_name' => __( '请输入 API Key 名称:', 'wpbridge' ),
|
||||||
|
'key_generated' => __( 'API Key 已生成,请妥善保存:', 'wpbridge' ),
|
||||||
|
'key_warning' => __( '此 Key 只会显示一次,请立即复制保存。', 'wpbridge' ),
|
||||||
|
'key_revoked' => __( 'API Key 已撤销', 'wpbridge' ),
|
||||||
|
// 诊断工具相关
|
||||||
|
'close_notice' => __( '关闭此通知', 'wpbridge' ),
|
||||||
|
'copied' => __( '已复制到剪贴板', 'wpbridge' ),
|
||||||
|
'diagnostics_complete' => __( '诊断完成', 'wpbridge' ),
|
||||||
|
'environment_ok' => __( '环境检查已完成', 'wpbridge' ),
|
||||||
|
'diagnostics_report' => __( 'WPBridge 诊断报告', 'wpbridge' ),
|
||||||
|
'generated_at' => __( '生成时间', 'wpbridge' ),
|
||||||
|
'system_info' => __( '系统信息', 'wpbridge' ),
|
||||||
|
'environment_check' => __( '环境检查', 'wpbridge' ),
|
||||||
|
'config_check' => __( '配置检查', 'wpbridge' ),
|
||||||
|
'source_status' => __( '更新源状态', 'wpbridge' ),
|
||||||
|
'passed' => __( '通过', 'wpbridge' ),
|
||||||
|
'warning' => __( '警告', 'wpbridge' ),
|
||||||
|
'not_tested' => __( '未测试', 'wpbridge' ),
|
||||||
|
'status' => __( '状态', 'wpbridge' ),
|
||||||
|
// 插件类型相关
|
||||||
|
'type_free' => __( '免费', 'wpbridge' ),
|
||||||
|
'type_commercial' => __( '商业', 'wpbridge' ),
|
||||||
|
'type_private' => __( '私有', 'wpbridge' ),
|
||||||
|
'type_unknown' => __( '第三方', 'wpbridge' ),
|
||||||
|
'type_saved' => __( '插件类型已保存', 'wpbridge' ),
|
||||||
|
'manual_mark' => __( '手动标记', 'wpbridge' ),
|
||||||
|
'manual_marked' => __( '当前为手动标记', 'wpbridge' ),
|
||||||
|
// 配置导入导出
|
||||||
|
'config_exported' => __( '配置已导出', 'wpbridge' ),
|
||||||
|
'config_imported' => __( '配置已导入', 'wpbridge' ),
|
||||||
|
'import_failed' => __( '导入失败', 'wpbridge' ),
|
||||||
|
'invalid_file' => __( '无效的配置文件', 'wpbridge' ),
|
||||||
|
'confirm_import' => __( '确定要导入配置吗?这将覆盖当前设置。', 'wpbridge' ),
|
||||||
|
// 版本锁定
|
||||||
|
'version_locked' => __( '版本已锁定', 'wpbridge' ),
|
||||||
|
'version_unlocked' => __( '版本已解锁', 'wpbridge' ),
|
||||||
|
'lock_current' => __( '锁定当前版本', 'wpbridge' ),
|
||||||
|
'lock_specific' => __( '锁定指定版本', 'wpbridge' ),
|
||||||
|
'lock_ignore' => __( '忽略特定版本', 'wpbridge' ),
|
||||||
|
'confirm_unlock' => __( '确定要解锁此版本吗?', 'wpbridge' ),
|
||||||
|
// 备份回滚
|
||||||
|
'rollback_success' => __( '回滚成功', 'wpbridge' ),
|
||||||
|
'rollback_failed' => __( '回滚失败', 'wpbridge' ),
|
||||||
|
'confirm_rollback' => __( '确定要回滚到此版本吗?当前版本将被覆盖。', 'wpbridge' ),
|
||||||
|
'no_backups' => __( '暂无备份', 'wpbridge' ),
|
||||||
|
// 更新日志
|
||||||
|
'changelog_title' => __( '更新日志', 'wpbridge' ),
|
||||||
|
'changelog_error' => __( '获取更新日志失败', 'wpbridge' ),
|
||||||
|
'loading' => __( '加载中...', 'wpbridge' ),
|
||||||
|
'last_updated' => __( '最后更新', 'wpbridge' ),
|
||||||
|
'recent_versions' => __( '最近版本', 'wpbridge' ),
|
||||||
|
'no_changelog' => __( '暂无更新日志', 'wpbridge' ),
|
||||||
|
// 模态框通用
|
||||||
|
'confirm_title' => __( '确认操作', 'wpbridge' ),
|
||||||
|
'confirm_btn' => __( '确定', 'wpbridge' ),
|
||||||
|
'cancel_btn' => __( '取消', 'wpbridge' ),
|
||||||
|
'delete_btn' => __( '删除', 'wpbridge' ),
|
||||||
|
'copy' => __( '复制', 'wpbridge' ),
|
||||||
|
// 删除更新源
|
||||||
|
'confirm_delete_title' => __( '删除更新源', 'wpbridge' ),
|
||||||
|
// API Key
|
||||||
|
'generate_api_key' => __( '生成 API Key', 'wpbridge' ),
|
||||||
|
'key_name_placeholder' => __( '例如:我的应用', 'wpbridge' ),
|
||||||
|
'key_name_required' => __( '请输入名称', 'wpbridge' ),
|
||||||
|
'key_generated_title' => __( 'API Key 已生成', 'wpbridge' ),
|
||||||
|
'revoke_key_title' => __( '撤销 API Key', 'wpbridge' ),
|
||||||
|
'revoke_btn' => __( '撤销', 'wpbridge' ),
|
||||||
|
// 清除日志
|
||||||
|
'clear_logs_title' => __( '清除日志', 'wpbridge' ),
|
||||||
|
'clear_btn' => __( '清除', 'wpbridge' ),
|
||||||
|
// 批量操作
|
||||||
|
'bulk_action_title' => __( '批量操作', 'wpbridge' ),
|
||||||
|
'confirm_bulk_action' => __( '确定要对选中的 {count} 个项目执行"{action}"操作吗?', 'wpbridge' ),
|
||||||
|
'action_set_source' => __( '设置更新源', 'wpbridge' ),
|
||||||
|
'action_reset' => __( '重置为默认', 'wpbridge' ),
|
||||||
|
'action_disable' => __( '禁用更新', 'wpbridge' ),
|
||||||
|
// 导入配置
|
||||||
|
'import_config_title' => __( '导入配置', 'wpbridge' ),
|
||||||
|
'import_btn' => __( '导入', 'wpbridge' ),
|
||||||
|
// 解锁版本
|
||||||
|
'unlock_version_title' => __( '解锁版本', 'wpbridge' ),
|
||||||
|
'unlock_btn' => __( '解锁', 'wpbridge' ),
|
||||||
|
// 异步检测
|
||||||
|
'no_plugins' => __( '没有插件需要检测', 'wpbridge' ),
|
||||||
|
'detecting' => __( '正在检测插件...', 'wpbridge' ),
|
||||||
|
'detection_complete' => __( '检测完成', 'wpbridge' ),
|
||||||
|
'progress' => __( '检测进度', 'wpbridge' ),
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加插件操作链接
|
||||||
|
*
|
||||||
|
* @param array $links 现有链接
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function add_action_links( array $links ): array {
|
||||||
|
$settings_link = sprintf(
|
||||||
|
'<a href="%s">%s</a>',
|
||||||
|
admin_url( 'admin.php?page=wpbridge' ),
|
||||||
|
__( '设置', 'wpbridge' )
|
||||||
|
);
|
||||||
|
array_unshift( $links, $settings_link );
|
||||||
|
return $links;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设置管理器
|
||||||
|
*
|
||||||
|
* @return Settings
|
||||||
|
*/
|
||||||
|
public function get_settings(): Settings {
|
||||||
|
return $this->settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 AI 网关
|
||||||
|
*
|
||||||
|
* @return AIGateway|null
|
||||||
|
*/
|
||||||
|
public function get_ai_gateway(): ?AIGateway {
|
||||||
|
return $this->ai_gateway;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取商业插件管理器
|
||||||
|
*
|
||||||
|
* @return CommercialManager|null
|
||||||
|
*/
|
||||||
|
public function get_commercial_manager(): ?CommercialManager {
|
||||||
|
return $this->commercial_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取通知管理器
|
||||||
|
*
|
||||||
|
* @return NotificationManager|null
|
||||||
|
*/
|
||||||
|
public function get_notification_manager(): ?NotificationManager {
|
||||||
|
return $this->notification_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取源分组管理器
|
||||||
|
*
|
||||||
|
* @return GroupManager|null
|
||||||
|
*/
|
||||||
|
public function get_group_manager(): ?GroupManager {
|
||||||
|
return $this->group_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 REST API 控制器
|
||||||
|
*
|
||||||
|
* @return RestController|null
|
||||||
|
*/
|
||||||
|
public function get_rest_controller(): ?RestController {
|
||||||
|
return $this->rest_controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取商业插件检测器
|
||||||
|
*
|
||||||
|
* @return CommercialDetector|null
|
||||||
|
*/
|
||||||
|
public function get_commercial_detector(): ?CommercialDetector {
|
||||||
|
return $this->commercial_detector;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 设置插件类型
|
||||||
|
*/
|
||||||
|
public function ajax_set_plugin_type(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugin_slug = isset( $_POST['plugin_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_slug'] ) ) : '';
|
||||||
|
$plugin_type = isset( $_POST['plugin_type'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_type'] ) ) : '';
|
||||||
|
|
||||||
|
if ( empty( $plugin_slug ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '插件标识不能为空', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $plugin_type ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '插件类型不能为空', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$detector = CommercialDetector::get_instance();
|
||||||
|
$result = $detector->set_user_mark( $plugin_slug, $plugin_type );
|
||||||
|
|
||||||
|
if ( $result ) {
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'message' => __( '插件类型已保存', 'wpbridge' ),
|
||||||
|
'type' => $plugin_type,
|
||||||
|
'label' => CommercialDetector::get_type_label( $plugin_type ),
|
||||||
|
) );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( array( 'message' => __( '保存失败', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 刷新商业插件检测
|
||||||
|
*/
|
||||||
|
public function ajax_refresh_commercial_detection(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$detector = CommercialDetector::get_instance();
|
||||||
|
$results = $detector->refresh_all();
|
||||||
|
|
||||||
|
$stats = array(
|
||||||
|
'total' => count( $results ),
|
||||||
|
'free' => 0,
|
||||||
|
'commercial' => 0,
|
||||||
|
'private' => 0,
|
||||||
|
'unknown' => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $results as $result ) {
|
||||||
|
if ( isset( $stats[ $result['type'] ] ) ) {
|
||||||
|
$stats[ $result['type'] ]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'message' => sprintf(
|
||||||
|
__( '已重新检测 %d 个插件:%d 免费,%d 商业,%d 第三方', 'wpbridge' ),
|
||||||
|
$stats['total'],
|
||||||
|
$stats['free'],
|
||||||
|
$stats['commercial'],
|
||||||
|
$stats['unknown'] + $stats['private']
|
||||||
|
),
|
||||||
|
'stats' => $stats,
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 准备刷新检测(返回插件列表)
|
||||||
|
*/
|
||||||
|
public function ajax_prepare_refresh(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$detector = CommercialDetector::get_instance();
|
||||||
|
$data = $detector->prepare_refresh();
|
||||||
|
|
||||||
|
wp_send_json_success( $data );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 批量检测插件
|
||||||
|
*/
|
||||||
|
public function ajax_refresh_batch(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $_POST['plugins'] ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '插件列表为空', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$plugins = json_decode( wp_unslash( $_POST['plugins'] ), true );
|
||||||
|
|
||||||
|
if ( ! is_array( $plugins ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '插件列表格式无效', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证并清理每个插件数据
|
||||||
|
$sanitized_plugins = array();
|
||||||
|
foreach ( $plugins as $plugin ) {
|
||||||
|
if ( ! isset( $plugin['slug'] ) || ! isset( $plugin['file'] ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sanitized_plugins[] = array(
|
||||||
|
'slug' => sanitize_text_field( $plugin['slug'] ),
|
||||||
|
'file' => sanitize_text_field( $plugin['file'] ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $sanitized_plugins ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '没有有效的插件数据', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$detector = CommercialDetector::get_instance();
|
||||||
|
$results = $detector->refresh_batch( $sanitized_plugins );
|
||||||
|
|
||||||
|
wp_send_json_success( array( 'results' => $results ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 导出配置
|
||||||
|
*/
|
||||||
|
public function ajax_export_config(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$include_secrets = isset( $_POST['include_secrets'] ) && 'true' === $_POST['include_secrets'];
|
||||||
|
|
||||||
|
$config_manager = new ConfigManager();
|
||||||
|
$config = $config_manager->export( $include_secrets );
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'config' => $config,
|
||||||
|
'filename' => 'wpbridge-config-' . gmdate( 'Y-m-d' ) . '.json',
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 导入配置
|
||||||
|
*/
|
||||||
|
public function ajax_import_config(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $_POST['config'] ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '配置数据为空', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = json_decode( wp_unslash( $_POST['config'] ), true );
|
||||||
|
|
||||||
|
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( 'JSON 格式无效', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$merge = isset( $_POST['merge'] ) && 'true' === $_POST['merge'];
|
||||||
|
|
||||||
|
$config_manager = new ConfigManager();
|
||||||
|
$result = $config_manager->import( $config, $merge );
|
||||||
|
|
||||||
|
if ( $result['success'] ) {
|
||||||
|
// 清除设置缓存
|
||||||
|
$this->settings->clear_cache();
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'message' => sprintf(
|
||||||
|
__( '成功导入 %d 项配置', 'wpbridge' ),
|
||||||
|
count( $result['imported'] )
|
||||||
|
),
|
||||||
|
'imported' => $result['imported'],
|
||||||
|
'skipped' => $result['skipped'],
|
||||||
|
) );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( array(
|
||||||
|
'message' => implode( ', ', $result['errors'] ),
|
||||||
|
'errors' => $result['errors'],
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 锁定版本
|
||||||
|
*/
|
||||||
|
public function ajax_lock_version(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_key = isset( $_POST['item_key'] ) ? sanitize_text_field( wp_unslash( $_POST['item_key'] ) ) : '';
|
||||||
|
$lock_type = isset( $_POST['lock_type'] ) ? sanitize_text_field( wp_unslash( $_POST['lock_type'] ) ) : '';
|
||||||
|
$version = isset( $_POST['version'] ) ? sanitize_text_field( wp_unslash( $_POST['version'] ) ) : '';
|
||||||
|
|
||||||
|
if ( empty( $item_key ) || empty( $lock_type ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '参数不完整', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$version_lock = VersionLock::get_instance();
|
||||||
|
|
||||||
|
if ( $version_lock->lock( $item_key, $lock_type, $version ) ) {
|
||||||
|
// 清除更新缓存
|
||||||
|
delete_site_transient( 'update_plugins' );
|
||||||
|
delete_site_transient( 'update_themes' );
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'message' => __( '版本已锁定', 'wpbridge' ),
|
||||||
|
'lock' => $version_lock->get( $item_key ),
|
||||||
|
) );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( array( 'message' => __( '锁定失败', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 解锁版本
|
||||||
|
*/
|
||||||
|
public function ajax_unlock_version(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_key = isset( $_POST['item_key'] ) ? sanitize_text_field( wp_unslash( $_POST['item_key'] ) ) : '';
|
||||||
|
|
||||||
|
if ( empty( $item_key ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '参数不完整', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$version_lock = VersionLock::get_instance();
|
||||||
|
|
||||||
|
if ( $version_lock->unlock( $item_key ) ) {
|
||||||
|
// 清除更新缓存
|
||||||
|
delete_site_transient( 'update_plugins' );
|
||||||
|
delete_site_transient( 'update_themes' );
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'message' => __( '版本已解锁', 'wpbridge' ),
|
||||||
|
) );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( array( 'message' => __( '解锁失败', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 回滚到备份
|
||||||
|
*/
|
||||||
|
public function ajax_rollback(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_key = isset( $_POST['item_key'] ) ? sanitize_text_field( wp_unslash( $_POST['item_key'] ) ) : '';
|
||||||
|
$backup_id = isset( $_POST['backup_id'] ) ? sanitize_text_field( wp_unslash( $_POST['backup_id'] ) ) : '';
|
||||||
|
|
||||||
|
if ( empty( $item_key ) || empty( $backup_id ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '参数不完整', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$backup_manager = BackupManager::get_instance();
|
||||||
|
$result = $backup_manager->rollback( $item_key, $backup_id );
|
||||||
|
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'message' => __( '回滚成功', 'wpbridge' ),
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 获取备份列表
|
||||||
|
*/
|
||||||
|
public function ajax_get_backups(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_key = isset( $_POST['item_key'] ) ? sanitize_text_field( wp_unslash( $_POST['item_key'] ) ) : '';
|
||||||
|
|
||||||
|
if ( empty( $item_key ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '参数不完整', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$backup_manager = BackupManager::get_instance();
|
||||||
|
$backups = $backup_manager->get_item_backups( $item_key );
|
||||||
|
|
||||||
|
wp_send_json_success( array(
|
||||||
|
'backups' => $backups,
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: 获取更新日志
|
||||||
|
*/
|
||||||
|
public function ajax_get_changelog(): void {
|
||||||
|
check_ajax_referer( 'wpbridge_nonce', 'nonce' );
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '权限不足', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = isset( $_POST['slug'] ) ? sanitize_text_field( wp_unslash( $_POST['slug'] ) ) : '';
|
||||||
|
$type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : 'plugin';
|
||||||
|
$source_type = isset( $_POST['source_type'] ) ? sanitize_text_field( wp_unslash( $_POST['source_type'] ) ) : 'wporg';
|
||||||
|
$source_url = isset( $_POST['source_url'] ) ? esc_url_raw( wp_unslash( $_POST['source_url'] ) ) : '';
|
||||||
|
|
||||||
|
if ( empty( $slug ) ) {
|
||||||
|
wp_send_json_error( array( 'message' => __( '参数不完整', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$changelog_manager = ChangelogManager::get_instance();
|
||||||
|
|
||||||
|
if ( 'theme' === $type ) {
|
||||||
|
$changelog = $changelog_manager->get_theme_changelog( $slug, $source_type, $source_url );
|
||||||
|
} else {
|
||||||
|
$changelog = $changelog_manager->get_plugin_changelog( $slug, $source_type, $source_url );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $changelog['success'] ) {
|
||||||
|
wp_send_json_error( array( 'message' => $changelog['error'] ?? __( '获取更新日志失败', 'wpbridge' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success( $changelog );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件激活
|
||||||
|
*/
|
||||||
|
public static function activate(): void {
|
||||||
|
// 创建默认设置
|
||||||
|
$settings = new Settings();
|
||||||
|
$settings->init_defaults();
|
||||||
|
|
||||||
|
// 清除更新缓存
|
||||||
|
delete_site_transient( 'update_plugins' );
|
||||||
|
delete_site_transient( 'update_themes' );
|
||||||
|
|
||||||
|
// 记录激活时间
|
||||||
|
update_option( 'wpbridge_activated', time() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件停用
|
||||||
|
*/
|
||||||
|
public static function deactivate(): void {
|
||||||
|
// 清除缓存
|
||||||
|
self::clear_all_cache();
|
||||||
|
|
||||||
|
// 移除定时任务
|
||||||
|
wp_clear_scheduled_hook( 'wpbridge_update_sources' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有缓存
|
||||||
|
*/
|
||||||
|
public static function clear_all_cache(): void {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// 清除所有 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() ) {
|
||||||
|
if ( function_exists( 'wp_cache_flush_group' ) ) {
|
||||||
|
wp_cache_flush_group( 'wpbridge' );
|
||||||
|
} else {
|
||||||
|
wp_cache_delete( 'wpbridge', 'wpbridge' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
297
includes/Core/RemoteConfig.php
Normal file
297
includes/Core/RemoteConfig.php
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 远程配置管理器
|
||||||
|
*
|
||||||
|
* 从远程服务器获取商业插件检测配置,支持:
|
||||||
|
* - 定时自动更新
|
||||||
|
* - 本地缓存
|
||||||
|
* - 降级到内置配置
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.7.5
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RemoteConfig 类
|
||||||
|
*/
|
||||||
|
class RemoteConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 远程配置 URL
|
||||||
|
*/
|
||||||
|
const CONFIG_URL = 'https://wpcy.com/api/bridge/commercial-config.json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存键名
|
||||||
|
*/
|
||||||
|
const CACHE_KEY = 'wpbridge_remote_config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存时间(秒)- 默认 12 小时
|
||||||
|
*/
|
||||||
|
const CACHE_TTL = 43200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单例实例
|
||||||
|
*
|
||||||
|
* @var RemoteConfig|null
|
||||||
|
*/
|
||||||
|
private static $instance = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
$this->load_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载配置(优先从缓存)
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 降级到内置配置
|
||||||
|
$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',
|
||||||
|
),
|
||||||
|
) );
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 ( ! $this->validate_config( $data ) ) {
|
||||||
|
Logger::warning( '远程配置结构无效' );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::info( '远程配置加载成功', array(
|
||||||
|
'version' => $data['version'] ?? 'unknown',
|
||||||
|
) );
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证配置结构
|
||||||
|
*
|
||||||
|
* @param array $data 配置数据
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function validate_config( $data ) {
|
||||||
|
if ( ! is_array( $data ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 必须包含版本号
|
||||||
|
if ( empty( $data['version'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
public function get_commercial_plugins() {
|
||||||
|
return $this->config['commercial_plugins'] ?? 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取商业框架列表
|
||||||
|
*
|
||||||
|
* @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_updated_at() {
|
||||||
|
return $this->config['updated_at'] ?? 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制刷新配置
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新失败,保持当前配置
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否使用内置配置
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function is_builtin() {
|
||||||
|
return strpos( $this->get_version(), 'builtin' ) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取完整配置
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_all() {
|
||||||
|
return $this->config;
|
||||||
|
}
|
||||||
|
}
|
||||||
350
includes/Core/Settings.php
Normal file
350
includes/Core/Settings.php
Normal file
|
|
@ -0,0 +1,350 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 设置管理
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置管理类
|
||||||
|
*/
|
||||||
|
class Settings {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项名称
|
||||||
|
*/
|
||||||
|
const OPTION_SOURCES = 'wpbridge_sources';
|
||||||
|
const OPTION_SETTINGS = 'wpbridge_settings';
|
||||||
|
const OPTION_AI_SETTINGS = 'wpbridge_ai_settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认设置
|
||||||
|
*
|
||||||
|
* @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_sources = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化默认设置
|
||||||
|
*/
|
||||||
|
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() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 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 [
|
||||||
|
[
|
||||||
|
'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, [] ),
|
||||||
|
$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 $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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新设置
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有更新源
|
||||||
|
*
|
||||||
|
* @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'] );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个更新源
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
// 生成唯一 ID
|
||||||
|
if ( empty( $source['id'] ) ) {
|
||||||
|
$source['id'] = 'source_' . wp_generate_uuid4();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
$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;
|
||||||
|
|
||||||
|
$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();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除更新源
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset( $sources[ $index ] );
|
||||||
|
$sources = array_values( $sources ); // 重新索引
|
||||||
|
|
||||||
|
$this->cached_sources = $sources;
|
||||||
|
return update_option( self::OPTION_SOURCES, $sources );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, [ 'enabled' => $enabled ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用调试模式
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求超时时间
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
306
includes/Core/SiteHealth.php
Normal file
306
includes/Core/SiteHealth.php
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WordPress Site Health 集成
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
use WPBridge\Cache\HealthChecker;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Site Health 集成类
|
||||||
|
*/
|
||||||
|
class SiteHealth {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
*/
|
||||||
|
public function __construct( Settings $settings ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks(): void {
|
||||||
|
// 添加健康检查测试
|
||||||
|
add_filter( 'site_status_tests', [ $this, 'add_tests' ] );
|
||||||
|
|
||||||
|
// 添加调试信息
|
||||||
|
add_filter( 'debug_information', [ $this, 'add_debug_info' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加健康检查测试
|
||||||
|
*
|
||||||
|
* @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'] = [
|
||||||
|
'label' => __( 'WPBridge 配置检查', 'wpbridge' ),
|
||||||
|
'test' => [ $this, 'test_config' ],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试更新源状态
|
||||||
|
*
|
||||||
|
* @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 = [];
|
||||||
|
|
||||||
|
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'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = count( $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 [
|
||||||
|
'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 [
|
||||||
|
'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 [
|
||||||
|
'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 = [];
|
||||||
|
|
||||||
|
// 检查调试模式
|
||||||
|
if ( $this->settings->is_debug() ) {
|
||||||
|
$issues[] = __( '调试模式已启用,建议在生产环境中关闭', 'wpbridge' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缓存时间
|
||||||
|
$cache_ttl = $this->settings->get_cache_ttl();
|
||||||
|
if ( $cache_ttl < 3600 ) {
|
||||||
|
$issues[] = __( '缓存时间设置过短,可能导致频繁请求', 'wpbridge' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查备份功能
|
||||||
|
if ( ! $this->settings->get( 'backup_enabled', true ) ) {
|
||||||
|
$issues[] = __( '更新前备份已禁用,建议启用以便回滚', 'wpbridge' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 ZipArchive
|
||||||
|
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||||
|
$issues[] = __( 'PHP ZipArchive 扩展未安装,备份功能将不可用', 'wpbridge' );
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [
|
||||||
|
'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();
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
362
includes/Core/SourceRegistry.php
Normal file
362
includes/Core/SourceRegistry.php
Normal file
|
|
@ -0,0 +1,362 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 源注册表管理
|
||||||
|
*
|
||||||
|
* 方案 B:项目优先架构 - 源注册表层
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.6.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源注册表管理类
|
||||||
|
*
|
||||||
|
* 管理所有可用的更新源(WP.org、FAIR、自定义等)
|
||||||
|
*/
|
||||||
|
class SourceRegistry {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项名称
|
||||||
|
*/
|
||||||
|
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 SIGNATURE_NONE = 'none';
|
||||||
|
const SIGNATURE_ED25519 = 'ed25519';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证类型
|
||||||
|
*/
|
||||||
|
const AUTH_NONE = 'none';
|
||||||
|
const AUTH_BASIC = 'basic';
|
||||||
|
const AUTH_BEARER = 'bearer';
|
||||||
|
const AUTH_TOKEN = 'token';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存的源列表
|
||||||
|
*
|
||||||
|
* @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, [] );
|
||||||
|
$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'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类型获取源
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 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();
|
||||||
|
|
||||||
|
if ( empty( $source['source_key'] ) ) {
|
||||||
|
$source['source_key'] = 'src_' . wp_generate_uuid4();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $this->get( $source['source_key'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新源
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除源
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用/禁用源
|
||||||
|
*
|
||||||
|
* @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, [
|
||||||
|
'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;
|
||||||
|
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预置源列表
|
||||||
|
*
|
||||||
|
* @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 [
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
301
includes/Core/VersionLock.php
Normal file
301
includes/Core/VersionLock.php
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 版本锁定管理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Core;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本锁定管理器类
|
||||||
|
*/
|
||||||
|
class VersionLock {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项名称
|
||||||
|
*/
|
||||||
|
const OPTION_NAME = 'wpbridge_version_locks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 锁定类型常量
|
||||||
|
*/
|
||||||
|
const LOCK_CURRENT = 'current'; // 锁定到当前版本
|
||||||
|
const LOCK_SPECIFIC = 'specific'; // 锁定到指定版本
|
||||||
|
const LOCK_IGNORE = 'ignore'; // 忽略特定版本
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单例实例
|
||||||
|
*
|
||||||
|
* @var VersionLock|null
|
||||||
|
*/
|
||||||
|
private static ?VersionLock $instance = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私有构造函数
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
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, [] );
|
||||||
|
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 项目键
|
||||||
|
* @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 ] = [
|
||||||
|
'type' => $lock_type,
|
||||||
|
'version' => $version,
|
||||||
|
'ignore_versions' => $ignore_versions,
|
||||||
|
'locked_at' => current_time( 'mysql' ),
|
||||||
|
];
|
||||||
|
|
||||||
|
$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();
|
||||||
|
|
||||||
|
if ( ! isset( $locks[ $item_key ] ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 项目键
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( $lock['type'] ) {
|
||||||
|
case self::LOCK_CURRENT:
|
||||||
|
// 锁定到当前版本,阻止所有更新
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case self::LOCK_SPECIFIC:
|
||||||
|
// 锁定到指定版本,如果当前版本等于锁定版本则阻止更新
|
||||||
|
return version_compare( $current_version, $lock['version'], '==' );
|
||||||
|
|
||||||
|
case self::LOCK_IGNORE:
|
||||||
|
// 忽略特定版本
|
||||||
|
return in_array( $new_version, $lock['ignore_versions'], true );
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤插件更新
|
||||||
|
*
|
||||||
|
* @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 ( ! 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' );
|
||||||
|
|
||||||
|
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
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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';
|
||||||
|
|
||||||
|
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
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取锁定类型标签
|
||||||
|
*
|
||||||
|
* @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 [
|
||||||
|
self::LOCK_CURRENT => __( '锁定当前版本', 'wpbridge' ),
|
||||||
|
self::LOCK_SPECIFIC => __( '锁定指定版本', 'wpbridge' ),
|
||||||
|
self::LOCK_IGNORE => __( '忽略特定版本', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存
|
||||||
|
*/
|
||||||
|
public function clear_cache(): void {
|
||||||
|
$this->locks = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
322
includes/FAIR/FairProtocol.php
Normal file
322
includes/FAIR/FairProtocol.php
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* FAIR 协议处理器
|
||||||
|
*
|
||||||
|
* 实现 FAIR (Federated And Independent Repositories) 协议支持
|
||||||
|
* 包括 DID 解析和 ED25519 签名验证
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.6.0
|
||||||
|
* @see https://github.com/nicholaswilson/fair-pm
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\FAIR;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAIR 协议处理器类
|
||||||
|
*/
|
||||||
|
class FairProtocol {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DID 方法前缀
|
||||||
|
*/
|
||||||
|
const DID_METHOD = 'did:fair:';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的签名方案
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode( ':', substr( $did, strlen( self::DID_METHOD ) ) );
|
||||||
|
|
||||||
|
if ( count( $parts ) < 2 ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if ( $version ) {
|
||||||
|
$did .= ':' . $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 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 );
|
||||||
|
|
||||||
|
if ( false === $signature_bin || false === $public_key_bin ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证签名长度
|
||||||
|
if ( strlen( $signature_bin ) !== SODIUM_CRYPTO_SIGN_BYTES ) {
|
||||||
|
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 );
|
||||||
|
|
||||||
|
} 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 );
|
||||||
|
|
||||||
|
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 false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证包签名
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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'] ?? '';
|
||||||
|
|
||||||
|
$result['algorithm'] = $algorithm;
|
||||||
|
$result['signer'] = $signer_did;
|
||||||
|
|
||||||
|
// 目前只支持 ED25519
|
||||||
|
if ( $algorithm !== self::SIGNATURE_ED25519 ) {
|
||||||
|
$result['error'] = 'unsupported_algorithm';
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建待验证消息
|
||||||
|
$message = $this->build_signature_message( $package );
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
if ( $this->verify_ed25519_signature( $message, $signature, $public_key ) ) {
|
||||||
|
$result['valid'] = true;
|
||||||
|
} else {
|
||||||
|
$result['error'] = 'invalid_signature';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建签名消息
|
||||||
|
*
|
||||||
|
* @param array $package 包数据
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function build_signature_message( array $package ): string {
|
||||||
|
// 移除签名字段
|
||||||
|
$data = $package;
|
||||||
|
unset( $data['signature'] );
|
||||||
|
|
||||||
|
// 按键排序
|
||||||
|
ksort( $data );
|
||||||
|
|
||||||
|
// 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 = [];
|
||||||
|
|
||||||
|
// FAIR 响应格式
|
||||||
|
if ( isset( $response['packages'] ) ) {
|
||||||
|
foreach ( $response['packages'] as $package ) {
|
||||||
|
$parsed = $this->parse_package( $package );
|
||||||
|
if ( $parsed ) {
|
||||||
|
$packages[] = $parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析单个包
|
||||||
|
*
|
||||||
|
* @param array $package 包数据
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
private function parse_package( array $package ): ?array {
|
||||||
|
if ( empty( $package['did'] ) && empty( $package['slug'] ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支持的签名算法
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_supported_algorithms(): array {
|
||||||
|
$algorithms = [];
|
||||||
|
|
||||||
|
if ( $this->is_sodium_available() ) {
|
||||||
|
$algorithms[] = self::SIGNATURE_ED25519;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $algorithms;
|
||||||
|
}
|
||||||
|
}
|
||||||
356
includes/FAIR/FairSourceAdapter.php
Normal file
356
includes/FAIR/FairSourceAdapter.php
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* FAIR 源适配器
|
||||||
|
*
|
||||||
|
* 处理 FAIR 协议源的更新检查和下载
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.6.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\FAIR;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use WPBridge\Core\SourceRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAIR 源适配器类
|
||||||
|
*/
|
||||||
|
class FairSourceAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAIR 协议处理器
|
||||||
|
*
|
||||||
|
* @var FairProtocol
|
||||||
|
*/
|
||||||
|
private FairProtocol $protocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源配置
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @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_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'] ?? '';
|
||||||
|
|
||||||
|
if ( empty( $api_url ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 API 请求 URL
|
||||||
|
$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug;
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
$response = $this->make_request( $endpoint );
|
||||||
|
|
||||||
|
if ( ! $response ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
$package = $this->parse_response( $response );
|
||||||
|
|
||||||
|
if ( ! $package ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查版本
|
||||||
|
if ( version_compare( $package['version'], $version, '<=' ) ) {
|
||||||
|
return null; // 没有更新
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证签名(如果需要)
|
||||||
|
if ( ! empty( $this->source['signature_required'] ) ) {
|
||||||
|
$verification = $this->protocol->verify_package_signature( $response );
|
||||||
|
|
||||||
|
if ( ! $verification['valid'] ) {
|
||||||
|
// 签名验证失败
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$package['signature_valid'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_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'] ?? '';
|
||||||
|
|
||||||
|
if ( empty( $api_url ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endpoint = trailingslashit( $api_url ) . $type . 's/' . $slug . '/info';
|
||||||
|
$response = $this->make_request( $endpoint );
|
||||||
|
|
||||||
|
if ( ! $response ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
if ( ! $parsed ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_url = $this->source['api_url'] ?? '';
|
||||||
|
|
||||||
|
if ( empty( $api_url ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAIR API 支持 DID 查询
|
||||||
|
$endpoint = trailingslashit( $api_url ) . 'resolve?did=' . urlencode( $did );
|
||||||
|
$response = $this->make_request( $endpoint );
|
||||||
|
|
||||||
|
if ( ! $response ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parse_response( $response );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 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['headers'] ) && is_array( $this->source['headers'] ) ) {
|
||||||
|
$args['headers'] = array_merge( $args['headers'], $this->source['headers'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code( $response );
|
||||||
|
|
||||||
|
if ( $code !== 200 ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
$data = json_decode( $body, true );
|
||||||
|
|
||||||
|
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取认证头
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取密钥
|
||||||
|
$secret = get_option( 'wpbridge_secret_' . $secret_ref, '' );
|
||||||
|
|
||||||
|
if ( empty( $secret ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( $auth_type ) {
|
||||||
|
case SourceRegistry::AUTH_BEARER:
|
||||||
|
return 'Bearer ' . $secret;
|
||||||
|
|
||||||
|
case SourceRegistry::AUTH_TOKEN:
|
||||||
|
return 'Token ' . $secret;
|
||||||
|
|
||||||
|
case SourceRegistry::AUTH_BASIC:
|
||||||
|
return 'Basic ' . base64_encode( $secret );
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析响应
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查包是否有签名
|
||||||
|
if ( empty( $package['signature'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算文件哈希
|
||||||
|
$file_hash = hash_file( 'sha256', $file_path );
|
||||||
|
|
||||||
|
// 验证哈希签名
|
||||||
|
$signature_data = $package['signature'];
|
||||||
|
|
||||||
|
if ( isset( $signature_data['file_hash'] ) ) {
|
||||||
|
if ( $signature_data['file_hash'] !== $file_hash ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
181
includes/Notification/EmailHandler.php
Normal file
181
includes/Notification/EmailHandler.php
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 邮件通知处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Notification;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件通知处理器类
|
||||||
|
*/
|
||||||
|
class EmailHandler implements HandlerInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的通知类型
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $supported_types = [ 'update', 'error', 'recovery' ];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
*/
|
||||||
|
public function __construct( Settings $settings ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取处理器名称
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_name(): string {
|
||||||
|
return 'email';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*
|
||||||
|
* @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', [] );
|
||||||
|
$enabled_types = $notification_settings['email']['types'] ?? $this->supported_types;
|
||||||
|
|
||||||
|
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 = [] ): void {
|
||||||
|
$notification_settings = $this->settings->get( 'notifications', [] );
|
||||||
|
$recipients = $notification_settings['email']['recipients'] ?? [];
|
||||||
|
|
||||||
|
if ( empty( $recipients ) ) {
|
||||||
|
// 默认发送给管理员
|
||||||
|
$recipients = [ get_option( 'admin_email' ) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证收件人邮箱格式
|
||||||
|
$valid_recipients = array_filter( $recipients, function ( $email ) {
|
||||||
|
return is_email( $email );
|
||||||
|
} );
|
||||||
|
|
||||||
|
if ( empty( $valid_recipients ) ) {
|
||||||
|
throw new \Exception( __( '没有有效的收件人邮箱', 'wpbridge' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 HTML 邮件
|
||||||
|
$html_message = $this->build_html_message( $subject, $message, $data );
|
||||||
|
|
||||||
|
// 使用 WordPress 默认发件人,避免 SPF/DKIM 问题
|
||||||
|
$headers = [
|
||||||
|
'Content-Type: text/html; charset=UTF-8',
|
||||||
|
];
|
||||||
|
|
||||||
|
$sent = wp_mail( $valid_recipients, $subject, $html_message, $headers );
|
||||||
|
|
||||||
|
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 = '<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>' . esc_html( $subject ) . '</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background: #0073aa; color: white; padding: 20px; text-align: center; }
|
||||||
|
.content { padding: 20px; background: #f9f9f9; }
|
||||||
|
.footer { padding: 20px; text-align: center; font-size: 12px; color: #666; }
|
||||||
|
.data-table { width: 100%; border-collapse: collapse; margin-top: 15px; }
|
||||||
|
.data-table th, .data-table td { padding: 8px; border: 1px solid #ddd; text-align: left; }
|
||||||
|
.data-table th { background: #f0f0f0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>WPBridge</h1>
|
||||||
|
</div>
|
||||||
|
<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>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>' . sprintf(
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
51
includes/Notification/HandlerInterface.php
Normal file
51
includes/Notification/HandlerInterface.php
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 通知处理器接口
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Notification;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知处理器接口
|
||||||
|
*/
|
||||||
|
interface HandlerInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送通知
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否支持该通知类型
|
||||||
|
*
|
||||||
|
* @param string $type 通知类型
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function supports_type( string $type ): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取处理器名称
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_name(): string;
|
||||||
|
}
|
||||||
231
includes/Notification/NotificationManager.php
Normal file
231
includes/Notification/NotificationManager.php
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 通知管理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Notification;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知管理器类
|
||||||
|
*/
|
||||||
|
class NotificationManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知处理器
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $handlers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
*/
|
||||||
|
public function __construct( Settings $settings ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
$this->init_handlers();
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化处理器
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
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 = [] ): 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 );
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
$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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源错误时触发
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
);
|
||||||
|
|
||||||
|
$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
|
||||||
|
);
|
||||||
|
|
||||||
|
$message = sprintf(
|
||||||
|
/* translators: %s: source ID */
|
||||||
|
__( '更新源 %s 已恢复正常', 'wpbridge' ),
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有处理器
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
|
||||||
|
if ( null === $handler ) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
320
includes/Notification/WebhookHandler.php
Normal file
320
includes/Notification/WebhookHandler.php
Normal file
|
|
@ -0,0 +1,320 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Webhook 通知处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Notification;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Security\Validator;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhook 通知处理器类
|
||||||
|
*/
|
||||||
|
class WebhookHandler implements HandlerInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的通知类型
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private array $supported_types = [ 'update', 'error', 'recovery' ];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
*/
|
||||||
|
public function __construct( Settings $settings ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取处理器名称
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_name(): string {
|
||||||
|
return 'webhook';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*
|
||||||
|
* @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', [] );
|
||||||
|
$enabled_types = $notification_settings['webhook']['types'] ?? $this->supported_types;
|
||||||
|
|
||||||
|
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 = [] ): 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' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 );
|
||||||
|
|
||||||
|
// 构建请求头
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
$response = wp_remote_post( $webhook_url, [
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => $payload,
|
||||||
|
'timeout' => 10,
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
throw new \Exception( $response->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
'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 );
|
||||||
|
|
||||||
|
case 'discord':
|
||||||
|
return $this->format_discord( $subject, $message, $data );
|
||||||
|
|
||||||
|
case 'teams':
|
||||||
|
return $this->format_teams( $subject, $message, $data );
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
'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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成签名
|
||||||
|
*
|
||||||
|
* @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 = [
|
||||||
|
'update' => 'good',
|
||||||
|
'error' => 'danger',
|
||||||
|
'recovery' => 'good',
|
||||||
|
'warning' => 'warning',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $colors[ $type ] ?? '#0073aa';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取类型对应的颜色(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取类型对应的颜色(十六进制格式)
|
||||||
|
*
|
||||||
|
* @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';
|
||||||
|
}
|
||||||
|
}
|
||||||
179
includes/Performance/BackgroundUpdater.php
Normal file
179
includes/Performance/BackgroundUpdater.php
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 后台更新器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Performance;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\UpdateSource\SourceManager;
|
||||||
|
use WPBridge\Cache\CacheManager;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台更新器类
|
||||||
|
* 使用 WP-Cron 在后台预先更新缓存
|
||||||
|
*/
|
||||||
|
class BackgroundUpdater {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务钩子名称
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const CRON_HOOK = 'wpbridge_update_sources';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源管理器
|
||||||
|
*
|
||||||
|
* @var SourceManager
|
||||||
|
*/
|
||||||
|
private SourceManager $source_manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 并行请求管理器
|
||||||
|
*
|
||||||
|
* @var ParallelRequestManager
|
||||||
|
*/
|
||||||
|
private ParallelRequestManager $parallel_manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理器
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
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 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( '开始后台更新' );
|
||||||
|
|
||||||
|
$sources = $this->source_manager->get_enabled_sorted();
|
||||||
|
|
||||||
|
if ( empty( $sources ) ) {
|
||||||
|
Logger::debug( '没有启用的更新源' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用并行请求检查所有源
|
||||||
|
$results = $this->parallel_manager->check_multiple_sources( $sources );
|
||||||
|
|
||||||
|
$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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::info( '后台更新完成', [
|
||||||
|
'success' => $success_count,
|
||||||
|
'failed' => $fail_count,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发更新
|
||||||
|
*
|
||||||
|
* @return array 更新结果
|
||||||
|
*/
|
||||||
|
public function trigger_update(): array {
|
||||||
|
$this->run_update();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'completed',
|
||||||
|
'time' => current_time( 'mysql' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下次更新时间
|
||||||
|
*
|
||||||
|
* @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 [
|
||||||
|
'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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
160
includes/Performance/ConditionalRequest.php
Normal file
160
includes/Performance/ConditionalRequest.php
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 条件请求处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Performance;
|
||||||
|
|
||||||
|
use WPBridge\Cache\CacheManager;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件请求处理器类
|
||||||
|
* 使用 ETag 和 Last-Modified 减少数据传输
|
||||||
|
*/
|
||||||
|
class ConditionalRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存管理器
|
||||||
|
*
|
||||||
|
* @var CacheManager
|
||||||
|
*/
|
||||||
|
private CacheManager $cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存前缀
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const CACHE_PREFIX = 'conditional_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
if ( ! empty( $cached['etag'] ) ) {
|
||||||
|
$headers['If-None-Match'] = $cached['etag'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $cached['last_modified'] ) ) {
|
||||||
|
$headers['If-Modified-Since'] = $cached['last_modified'];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
if ( ! empty( $headers['etag'] ) ) {
|
||||||
|
$metadata['etag'] = $headers['etag'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $headers['last-modified'] ) ) {
|
||||||
|
$metadata['last_modified'] = $headers['last-modified'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $metadata ) ) {
|
||||||
|
$this->save_metadata( $source_id, $metadata );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有新数据,缓存并返回
|
||||||
|
if ( null !== $response ) {
|
||||||
|
$this->save_cached_data( $source_id, $response );
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回缓存数据
|
||||||
|
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 : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存元数据
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* @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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
163
includes/Performance/ParallelRequestManager.php
Normal file
163
includes/Performance/ParallelRequestManager.php
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 并行请求管理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Performance;
|
||||||
|
|
||||||
|
use WPBridge\UpdateSource\SourceModel;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 并行请求管理器类
|
||||||
|
* 使用 WordPress Requests API 实现并行请求
|
||||||
|
*/
|
||||||
|
class ParallelRequestManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认超时时间(秒)
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private int $timeout = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @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 [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$requests = [];
|
||||||
|
|
||||||
|
foreach ( $sources as $source ) {
|
||||||
|
$requests[ $source->id ] = [
|
||||||
|
'url' => $source->get_check_url(),
|
||||||
|
'type' => \WpOrg\Requests\Requests::GET,
|
||||||
|
'headers' => $source->get_headers(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::debug( '开始并行请求', [ 'count' => count( $requests ) ] );
|
||||||
|
|
||||||
|
$start = microtime( true );
|
||||||
|
|
||||||
|
// 使用 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 );
|
||||||
|
|
||||||
|
Logger::debug( '并行请求完成', [
|
||||||
|
'count' => count( $requests ),
|
||||||
|
'time_ms' => $elapsed,
|
||||||
|
] );
|
||||||
|
|
||||||
|
return $this->process_responses( $responses );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理响应
|
||||||
|
*
|
||||||
|
* @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( '请求失败', [
|
||||||
|
'source' => $source_id,
|
||||||
|
'error' => $response->getMessage(),
|
||||||
|
] );
|
||||||
|
$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 );
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量请求 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 = [];
|
||||||
|
|
||||||
|
foreach ( $urls as $key => $url ) {
|
||||||
|
$requests[ $key ] = [
|
||||||
|
'url' => $url,
|
||||||
|
'type' => \WpOrg\Requests\Requests::GET,
|
||||||
|
'headers' => $headers,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$responses = \WpOrg\Requests\Requests::request_multiple(
|
||||||
|
$requests,
|
||||||
|
[
|
||||||
|
'timeout' => $this->timeout,
|
||||||
|
'connect_timeout' => 5,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->process_responses( $responses );
|
||||||
|
}
|
||||||
|
}
|
||||||
118
includes/Performance/RequestDeduplicator.php
Normal file
118
includes/Performance/RequestDeduplicator.php
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 请求去重器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Performance;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求去重器类
|
||||||
|
* 防止短时间内重复请求同一源
|
||||||
|
*/
|
||||||
|
class RequestDeduplicator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并窗口时间(秒)
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
const MERGE_WINDOW = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 锁前缀
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
// 检查是否已有锁
|
||||||
|
if ( get_transient( $lock_key ) ) {
|
||||||
|
Logger::debug( '请求被去重', [ 'source' => $source_id ] );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置锁
|
||||||
|
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
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
while ( $this->has_lock( $source_id ) ) {
|
||||||
|
if ( ( time() - $start ) >= $max_wait ) {
|
||||||
|
Logger::warning( '等待锁超时', [ 'source' => $source_id ] );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
usleep( 100000 ); // 100ms
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $callback();
|
||||||
|
return $result;
|
||||||
|
} finally {
|
||||||
|
$this->release_lock( $source_id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
212
includes/Security/Encryption.php
Normal file
212
includes/Security/Encryption.php
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 密钥加密
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Security;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密钥加密类
|
||||||
|
*/
|
||||||
|
class Encryption {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密方法
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果都没有,生成并存储一个随机密钥
|
||||||
|
$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 '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 );
|
||||||
|
|
||||||
|
if ( false === $encrypted ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 IV 和加密数据一起存储
|
||||||
|
return base64_encode( $iv . $encrypted );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密数据
|
||||||
|
*
|
||||||
|
* @param string $data 加密数据(base64 编码)
|
||||||
|
* @return string 解密后的明文
|
||||||
|
*/
|
||||||
|
public static function decrypt( string $data ): string {
|
||||||
|
if ( empty( $data ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = base64_decode( $data );
|
||||||
|
|
||||||
|
if ( false === $data ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = hash( 'sha256', self::get_key(), true );
|
||||||
|
$iv_length = openssl_cipher_iv_length( self::METHOD );
|
||||||
|
|
||||||
|
if ( strlen( $data ) < $iv_length ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$iv = substr( $data, 0, $iv_length );
|
||||||
|
$encrypted = substr( $data, $iv_length );
|
||||||
|
|
||||||
|
$decrypted = openssl_decrypt( $encrypted, self::METHOD, $key, OPENSSL_RAW_DATA, $iv );
|
||||||
|
|
||||||
|
if ( false === $decrypted ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查数据是否已加密
|
||||||
|
*
|
||||||
|
* @param string $data 数据
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_encrypted( string $data ): bool {
|
||||||
|
if ( empty( $data ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是有效的 base64
|
||||||
|
$decoded = base64_decode( $data, true );
|
||||||
|
|
||||||
|
if ( false === $decoded ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查长度是否足够包含 IV
|
||||||
|
$iv_length = openssl_cipher_iv_length( self::METHOD );
|
||||||
|
|
||||||
|
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 $default 默认值
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function get_secure( string $key, string $default = '' ): string {
|
||||||
|
$encrypted = get_option( 'wpbridge_secure_' . $key, '' );
|
||||||
|
|
||||||
|
if ( empty( $encrypted ) ) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decrypted = self::decrypt( $encrypted );
|
||||||
|
|
||||||
|
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 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 原始数据
|
||||||
|
* @param string $hash 哈希值
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function verify_hash( string $data, string $hash ): bool {
|
||||||
|
return hash_equals( self::hash( $data ), $hash );
|
||||||
|
}
|
||||||
|
}
|
||||||
229
includes/Security/Validator.php
Normal file
229
includes/Security/Validator.php
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 输入校验
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\Security;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入校验类
|
||||||
|
*/
|
||||||
|
class Validator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只允许 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁止本地地址(安全考虑)
|
||||||
|
if ( self::is_local_address( $host ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否是本地地址
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 私有 IP 范围
|
||||||
|
$ip = gethostbyname( $host );
|
||||||
|
|
||||||
|
if ( $ip === $host ) {
|
||||||
|
// 无法解析,为安全起见视为本地地址
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查私有 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查 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 );
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持语义化版本和 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 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-]+$/';
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
foreach ( $required as $field ) {
|
||||||
|
if ( ! isset( $data[ $field ] ) ) {
|
||||||
|
$errors[] = sprintf( __( '缺少必需字段: %s', 'wpbridge' ), $field );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验更新信息 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' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载 URL
|
||||||
|
$download_url = $data['download_url'] ?? $data['package'] ?? '';
|
||||||
|
if ( ! empty( $download_url ) && ! self::is_valid_url( $download_url ) ) {
|
||||||
|
$errors[] = __( '无效的下载 URL', 'wpbridge' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理 URL
|
||||||
|
*
|
||||||
|
* @param string $url URL
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function sanitize_url( string $url ): string {
|
||||||
|
return esc_url_raw( $url );
|
||||||
|
}
|
||||||
|
}
|
||||||
354
includes/SourceGroup/GroupManager.php
Normal file
354
includes/SourceGroup/GroupManager.php
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 源分组管理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\SourceGroup;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Security\Encryption;
|
||||||
|
use WPBridge\UpdateSource\SourceManager;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源分组管理器类
|
||||||
|
*/
|
||||||
|
class GroupManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项名称
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const OPTION_NAME = 'wpbridge_source_groups';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源管理器
|
||||||
|
*
|
||||||
|
* @var SourceManager
|
||||||
|
*/
|
||||||
|
private SourceManager $source_manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @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, [] );
|
||||||
|
$groups = [];
|
||||||
|
|
||||||
|
foreach ( $groups_data as $data ) {
|
||||||
|
$groups[] = GroupModel::from_array( $data );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个分组
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加分组
|
||||||
|
*
|
||||||
|
* @param GroupModel $group 分组模型
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function add( GroupModel $group ): bool {
|
||||||
|
if ( ! $group->is_valid() ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups = $this->get_all();
|
||||||
|
|
||||||
|
// 生成 ID
|
||||||
|
if ( empty( $group->id ) ) {
|
||||||
|
$group->id = 'group_' . wp_generate_uuid4();
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups[] = $group;
|
||||||
|
|
||||||
|
Logger::info( '添加源分组', [ 'id' => $group->id, 'name' => $group->name ] );
|
||||||
|
|
||||||
|
return $this->save_groups( $groups );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分组
|
||||||
|
*
|
||||||
|
* @param GroupModel $group 分组模型
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function update( GroupModel $group ): bool {
|
||||||
|
if ( ! $group->is_valid() ) {
|
||||||
|
return 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;
|
||||||
|
|
||||||
|
// 处理共享认证令牌
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $found ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::info( '更新源分组', [ 'id' => $group->id ] );
|
||||||
|
|
||||||
|
return $this->save_groups( $groups );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分组
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( count( $new_groups ) === count( $groups ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::info( '删除源分组', [ 'id' => $id ] );
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
if ( null === $group ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先更新分组状态
|
||||||
|
$group->enabled = $enabled;
|
||||||
|
if ( ! $this->update( $group ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 然后批量更新源状态,记录失败的源
|
||||||
|
$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( '部分源状态切换失败', [
|
||||||
|
'group_id' => $id,
|
||||||
|
'failed' => $failed_sources,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分组内的所有源
|
||||||
|
*
|
||||||
|
* @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 [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sources = [];
|
||||||
|
foreach ( $group->source_ids as $source_id ) {
|
||||||
|
$source = $this->source_manager->get( $source_id );
|
||||||
|
if ( null !== $source ) {
|
||||||
|
$sources[] = $source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$group = $this->get( $group_id );
|
||||||
|
|
||||||
|
if ( null === $group ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$group->add_source( $source_id );
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$group = $this->get( $group_id );
|
||||||
|
|
||||||
|
if ( null === $group ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$group->remove_source( $source_id );
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
foreach ( $groups as $group ) {
|
||||||
|
if ( $group->has_source( $source_id ) ) {
|
||||||
|
$source_groups[] = $group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统计信息
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => $total,
|
||||||
|
'enabled' => $enabled,
|
||||||
|
'disabled' => $total - $enabled,
|
||||||
|
'total_sources' => $total_sources,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
199
includes/SourceGroup/GroupModel.php
Normal file
199
includes/SourceGroup/GroupModel.php
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 源分组数据模型
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\SourceGroup;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源分组模型类
|
||||||
|
*/
|
||||||
|
class GroupModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 唯一标识
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $id = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分组名称
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $name = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分组描述
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $description = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 包含的源 ID 列表
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public array $source_ids = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享认证令牌
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $shared_auth_token = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public bool $enabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $created_at = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $updated_at = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数组创建实例
|
||||||
|
*
|
||||||
|
* @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'] ?? '' );
|
||||||
|
|
||||||
|
// 验证 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'] ?? '' );
|
||||||
|
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为数组
|
||||||
|
*
|
||||||
|
* @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 ) ? '' : '********' );
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证模型
|
||||||
|
*
|
||||||
|
* @return array 错误数组
|
||||||
|
*/
|
||||||
|
public function validate(): array {
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ( empty( $this->name ) ) {
|
||||||
|
$errors['name'] = __( '分组名称不能为空', 'wpbridge' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有效
|
||||||
|
*
|
||||||
|
* @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 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取源数量
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function get_source_count(): int {
|
||||||
|
return count( $this->source_ids );
|
||||||
|
}
|
||||||
|
}
|
||||||
239
includes/UpdateSource/Handlers/AbstractHandler.php
Normal file
239
includes/UpdateSource/Handlers/AbstractHandler.php
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 抽象处理器基类
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\UpdateSource\SourceModel;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Security\Encryption;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽象处理器基类
|
||||||
|
*/
|
||||||
|
abstract class AbstractHandler implements HandlerInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源模型
|
||||||
|
*
|
||||||
|
* @var SourceModel
|
||||||
|
*/
|
||||||
|
protected SourceModel $source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求超时时间(秒)
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected int $timeout = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param SourceModel $source 源模型
|
||||||
|
*/
|
||||||
|
public function __construct( SourceModel $source ) {
|
||||||
|
$this->source = $source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求头
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_headers(): array {
|
||||||
|
return $this->source->get_headers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证认证信息
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function validate_auth(): bool {
|
||||||
|
// 默认不需要认证
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连通性
|
||||||
|
*
|
||||||
|
* @return HealthStatus
|
||||||
|
*/
|
||||||
|
public function test_connection(): HealthStatus {
|
||||||
|
$start = microtime( true );
|
||||||
|
|
||||||
|
$response = wp_remote_get( $this->get_check_url(), [
|
||||||
|
'timeout' => $this->timeout,
|
||||||
|
'headers' => $this->get_headers(),
|
||||||
|
] );
|
||||||
|
|
||||||
|
$elapsed = (int) ( ( microtime( true ) - $start ) * 1000 );
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return HealthStatus::failed( $response->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code( $response );
|
||||||
|
|
||||||
|
if ( $code >= 200 && $code < 300 ) {
|
||||||
|
return HealthStatus::healthy( $elapsed );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $code >= 500 ) {
|
||||||
|
return HealthStatus::failed( 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 {
|
||||||
|
$defaults = [
|
||||||
|
'timeout' => $this->timeout,
|
||||||
|
'headers' => $this->get_headers(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$args = wp_parse_args( $args, $defaults );
|
||||||
|
$response = wp_remote_get( $url, $args );
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
if ( $code < 200 || $code >= 300 ) {
|
||||||
|
Logger::warning( '请求返回非 2xx 状态码', [
|
||||||
|
'url' => $this->redact_url( $url ),
|
||||||
|
'code' => $code,
|
||||||
|
] );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode( $body, true );
|
||||||
|
|
||||||
|
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||||
|
Logger::error( 'JSON 解析失败', [
|
||||||
|
'url' => $this->redact_url( $url ),
|
||||||
|
'error' => json_last_error_msg(),
|
||||||
|
] );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, '>' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取解密后的认证令牌
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function get_auth_token(): string {
|
||||||
|
if ( empty( $this->source->auth_token ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = Encryption::decrypt( $this->source->auth_token );
|
||||||
|
|
||||||
|
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 $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;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_str( $parts['query'], $query );
|
||||||
|
if ( empty( $query ) ) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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'] : '';
|
||||||
|
|
||||||
|
return $scheme . $auth . $host . $port . $path . ( $querystr ? '?' . $querystr : '' ) . $fragment;
|
||||||
|
}
|
||||||
|
}
|
||||||
178
includes/UpdateSource/Handlers/ArkPressHandler.php
Normal file
178
includes/UpdateSource/Handlers/ArkPressHandler.php
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ArkPress 处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArkPress 处理器
|
||||||
|
* 文派自托管方案,AspireCloud 分叉版本
|
||||||
|
*/
|
||||||
|
class ArkPressHandler extends AbstractHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @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';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查更新
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArkPress API 响应格式
|
||||||
|
$remote_version = $data['version'] ?? $data['new_version'] ?? '';
|
||||||
|
|
||||||
|
if ( empty( $remote_version ) ) {
|
||||||
|
Logger::warning( 'ArkPress 响应缺少版本信息', [ 'url' => $url, 'slug' => $slug ] );
|
||||||
|
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'] ?? [];
|
||||||
|
$info->banners = $data['banners'] ?? [];
|
||||||
|
|
||||||
|
if ( isset( $data['sections'] ) ) {
|
||||||
|
$info->changelog = $data['sections']['changelog'] ?? '';
|
||||||
|
$info->description = $data['sections']['description'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::info( 'ArkPress: 发现更新', [
|
||||||
|
'slug' => $slug,
|
||||||
|
'current' => $version,
|
||||||
|
'new' => $remote_version,
|
||||||
|
] );
|
||||||
|
|
||||||
|
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 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, [
|
||||||
|
'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 批量检查失败', [
|
||||||
|
'error' => $response->get_error_message(),
|
||||||
|
] );
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
$data = json_decode( $body, true );
|
||||||
|
|
||||||
|
if ( ! is_array( $data ) || ! isset( $data['plugins'] ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
112
includes/UpdateSource/Handlers/AspireCloudHandler.php
Normal file
112
includes/UpdateSource/Handlers/AspireCloudHandler.php
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AspireCloud 处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AspireCloud 处理器
|
||||||
|
*/
|
||||||
|
class AspireCloudHandler extends AbstractHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @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';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查更新
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AspireCloud API 响应格式
|
||||||
|
$remote_version = $data['version'] ?? $data['new_version'] ?? '';
|
||||||
|
|
||||||
|
if ( empty( $remote_version ) ) {
|
||||||
|
Logger::warning( 'AspireCloud 响应缺少版本信息', [ 'url' => $url, 'slug' => $slug ] );
|
||||||
|
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;
|
||||||
|
|
||||||
|
Logger::info( 'AspireCloud: 发现更新', [
|
||||||
|
'slug' => $slug,
|
||||||
|
'current' => $version,
|
||||||
|
'new' => $remote_version,
|
||||||
|
] );
|
||||||
|
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建插件 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
179
includes/UpdateSource/Handlers/BridgeServerHandler.php
Normal file
179
includes/UpdateSource/Handlers/BridgeServerHandler.php
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bridge Server 处理器
|
||||||
|
*
|
||||||
|
* 通过 wpbridge-server Go 服务获取商业插件更新
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.8
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\UpdateSource\SourceModel;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Commercial\BridgeClient;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridge Server 处理器类
|
||||||
|
*/
|
||||||
|
class BridgeServerHandler extends AbstractHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridge 客户端
|
||||||
|
*
|
||||||
|
* @var BridgeClient|null
|
||||||
|
*/
|
||||||
|
private ?BridgeClient $client = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param SourceModel $source 源模型
|
||||||
|
*/
|
||||||
|
public function __construct( SourceModel $source ) {
|
||||||
|
parent::__construct( $source );
|
||||||
|
|
||||||
|
// 从 source 配置初始化客户端
|
||||||
|
$server_url = $source->api_url;
|
||||||
|
$api_key = $this->get_auth_token();
|
||||||
|
|
||||||
|
if ( ! empty( $server_url ) ) {
|
||||||
|
$this->client = new BridgeClient( $server_url, $api_key );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_capabilities(): array {
|
||||||
|
return [
|
||||||
|
'auth' => 'api_key',
|
||||||
|
'version' => 'json',
|
||||||
|
'download' => 'signed_url',
|
||||||
|
'batch' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检查 URL
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_check_url(): string {
|
||||||
|
return rtrim( $this->source->api_url, '/' ) . '/api/v1/health';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查更新
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @param string $version 当前版本
|
||||||
|
* @return UpdateInfo|null
|
||||||
|
*/
|
||||||
|
public function check_update( string $slug, string $version ): ?UpdateInfo {
|
||||||
|
if ( ! $this->client || ! $this->client->is_configured() ) {
|
||||||
|
Logger::warning( 'Bridge Server 未配置', [ 'slug' => $slug ] );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = $this->client->get_plugin_info( $slug );
|
||||||
|
|
||||||
|
if ( empty( $info ) || empty( $info['version'] ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 比较版本
|
||||||
|
if ( ! $this->is_newer_version( $version, $info['version'] ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取签名下载 URL
|
||||||
|
$download_url = $this->client->get_download_url( $slug );
|
||||||
|
|
||||||
|
if ( empty( $download_url ) ) {
|
||||||
|
Logger::warning( 'Bridge Server 无法获取下载 URL', [ 'slug' => $slug ] );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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'] ?? '',
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目信息
|
||||||
|
*
|
||||||
|
* @param string $slug 插件 slug
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function get_info( string $slug ): ?array {
|
||||||
|
if ( ! $this->client || ! $this->client->is_configured() ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = $this->client->get_plugin_info( $slug );
|
||||||
|
|
||||||
|
if ( empty( $info ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加下载 URL
|
||||||
|
$info['download_url'] = $this->client->get_download_url( $slug );
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证认证信息
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function validate_auth(): bool {
|
||||||
|
if ( ! $this->client ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->client->health_check();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连通性
|
||||||
|
*
|
||||||
|
* @return HealthStatus
|
||||||
|
*/
|
||||||
|
public function test_connection(): HealthStatus {
|
||||||
|
if ( ! $this->client ) {
|
||||||
|
return HealthStatus::failed( 'Bridge Server 未配置' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = microtime( true );
|
||||||
|
|
||||||
|
if ( $this->client->health_check() ) {
|
||||||
|
$elapsed = (int) ( ( microtime( true ) - $start ) * 1000 );
|
||||||
|
return HealthStatus::healthy( $elapsed );
|
||||||
|
}
|
||||||
|
|
||||||
|
return HealthStatus::failed( '连接失败' );
|
||||||
|
}
|
||||||
|
}
|
||||||
114
includes/UpdateSource/Handlers/FairHandler.php
Normal file
114
includes/UpdateSource/Handlers/FairHandler.php
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* FAIR 处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\Core\SourceRegistry;
|
||||||
|
use WPBridge\FAIR\FairSourceAdapter;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAIR 协议处理器
|
||||||
|
*/
|
||||||
|
class FairHandler extends AbstractHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
$data = $this->source->item_type === 'theme'
|
||||||
|
? $adapter->check_theme_update( $slug, $version )
|
||||||
|
: $adapter->check_plugin_update( $slug, $version );
|
||||||
|
|
||||||
|
if ( null === $data ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = UpdateInfo::from_array( $data );
|
||||||
|
$info->slug = $slug;
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目信息
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 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 = [
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
238
includes/UpdateSource/Handlers/GitHubHandler.php
Normal file
238
includes/UpdateSource/Handlers/GitHubHandler.php
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* GitHub 处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub 处理器类
|
||||||
|
*/
|
||||||
|
class GitHubHandler extends AbstractHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub API 基础 URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const API_BASE = 'https://api.github.com';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检查 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 );
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
if ( null === $data ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析版本号(去除 v 前缀)
|
||||||
|
$remote_version = $data['tag_name'] ?? '';
|
||||||
|
$remote_version = ltrim( $remote_version, 'v' );
|
||||||
|
|
||||||
|
if ( empty( $remote_version ) ) {
|
||||||
|
Logger::warning( 'GitHub: 响应缺少版本信息', [ 'repo' => $repo ] );
|
||||||
|
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 );
|
||||||
|
|
||||||
|
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'] ?? '';
|
||||||
|
|
||||||
|
Logger::info( 'GitHub: 发现更新', [
|
||||||
|
'repo' => $repo,
|
||||||
|
'current' => $version,
|
||||||
|
'new' => $remote_version,
|
||||||
|
] );
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取仓库信息
|
||||||
|
$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 );
|
||||||
|
|
||||||
|
if ( null === $repo_data || null === $release_data ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$version = ltrim( $release_data['tag_name'] ?? '', 'v' );
|
||||||
|
|
||||||
|
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 = trim( $url );
|
||||||
|
|
||||||
|
// 移除协议
|
||||||
|
$url = preg_replace( '#^https?://#', '', $url );
|
||||||
|
|
||||||
|
// 移除 github.com
|
||||||
|
$url = preg_replace( '#^github\.com/#', '', $url );
|
||||||
|
|
||||||
|
// 移除 .git 后缀
|
||||||
|
$url = preg_replace( '#\.git$#', '', $url );
|
||||||
|
|
||||||
|
// 验证格式
|
||||||
|
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
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'] ?? '';
|
||||||
|
|
||||||
|
// 匹配 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 zipball_url 作为后备
|
||||||
|
return $release['zipball_url'] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
250
includes/UpdateSource/Handlers/GitLabHandler.php
Normal file
250
includes/UpdateSource/Handlers/GitLabHandler.php
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* GitLab 处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitLab 处理器类
|
||||||
|
*/
|
||||||
|
class GitLabHandler extends AbstractHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitLab API 基础 URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const API_BASE = 'https://gitlab.com/api/v4';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_capabilities(): array {
|
||||||
|
return [
|
||||||
|
'auth' => 'token',
|
||||||
|
'version' => 'release',
|
||||||
|
'download' => 'release',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求头
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_headers(): array {
|
||||||
|
$headers = [];
|
||||||
|
|
||||||
|
$token = $this->get_auth_token();
|
||||||
|
if ( ! empty( $token ) ) {
|
||||||
|
// GitLab 使用 PRIVATE-TOKEN 头
|
||||||
|
$headers['PRIVATE-TOKEN'] = $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查更新
|
||||||
|
*
|
||||||
|
* @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', [ 'url' => $this->source->api_url ] );
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = self::API_BASE . '/projects/' . $project_id . '/releases';
|
||||||
|
$data = $this->request( $url );
|
||||||
|
|
||||||
|
if ( null === $data || empty( $data ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最新 Release(第一个)
|
||||||
|
$latest = $data[0] ?? null;
|
||||||
|
|
||||||
|
if ( null === $latest ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析版本号
|
||||||
|
$remote_version = $latest['tag_name'] ?? '';
|
||||||
|
$remote_version = ltrim( $remote_version, 'v' );
|
||||||
|
|
||||||
|
if ( empty( $remote_version ) ) {
|
||||||
|
Logger::warning( 'GitLab: 响应缺少版本信息', [ 'project' => $project_id ] );
|
||||||
|
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 );
|
||||||
|
|
||||||
|
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'] ?? '';
|
||||||
|
|
||||||
|
Logger::info( 'GitLab: 发现更新', [
|
||||||
|
'project' => $project_id,
|
||||||
|
'current' => $version,
|
||||||
|
'new' => $remote_version,
|
||||||
|
] );
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目信息
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取项目信息
|
||||||
|
$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 );
|
||||||
|
|
||||||
|
if ( null === $project_data ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$latest = $releases_data[0] ?? [];
|
||||||
|
$version = ltrim( $latest['tag_name'] ?? '', 'v' );
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
// 移除协议
|
||||||
|
$url = preg_replace( '#^https?://#', '', $url );
|
||||||
|
|
||||||
|
// 移除 gitlab.com
|
||||||
|
$url = preg_replace( '#^gitlab\.com/#', '', $url );
|
||||||
|
|
||||||
|
// 移除 .git 后缀
|
||||||
|
$url = preg_replace( '#\.git$#', '', $url );
|
||||||
|
|
||||||
|
// URL 编码路径
|
||||||
|
if ( preg_match( '#^[\w.-]+/[\w.-]+(?:/[\w.-]+)*$#', $url ) ) {
|
||||||
|
return urlencode( $url );
|
||||||
|
}
|
||||||
|
|
||||||
|
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'] ?? '';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用归档 URL 作为后备
|
||||||
|
$tag = $release['tag_name'] ?? '';
|
||||||
|
if ( ! empty( $tag ) ) {
|
||||||
|
return self::API_BASE . '/projects/' . $project_id . '/repository/archive.zip?sha=' . $tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
252
includes/UpdateSource/Handlers/GiteeHandler.php
Normal file
252
includes/UpdateSource/Handlers/GiteeHandler.php
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Gitee 处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gitee 处理器类(国内 Git 平台)
|
||||||
|
*/
|
||||||
|
class GiteeHandler extends AbstractHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gitee API 基础 URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const API_BASE = 'https://gitee.com/api/v5';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检查 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 );
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
$token = $this->get_auth_token();
|
||||||
|
if ( ! empty( $token ) ) {
|
||||||
|
$url = add_query_arg( 'access_token', $token, $url );
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->request( $url );
|
||||||
|
|
||||||
|
if ( null === $data ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析版本号
|
||||||
|
$remote_version = $data['tag_name'] ?? '';
|
||||||
|
$remote_version = ltrim( $remote_version, 'v' );
|
||||||
|
|
||||||
|
if ( empty( $remote_version ) ) {
|
||||||
|
Logger::warning( 'Gitee: 响应缺少版本信息', [ 'repo' => $repo ] );
|
||||||
|
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 );
|
||||||
|
|
||||||
|
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'] ?? '';
|
||||||
|
|
||||||
|
Logger::info( 'Gitee: 发现更新', [
|
||||||
|
'repo' => $repo,
|
||||||
|
'current' => $version,
|
||||||
|
'new' => $remote_version,
|
||||||
|
] );
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取仓库信息
|
||||||
|
$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 );
|
||||||
|
|
||||||
|
if ( null === $repo_data ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$version = ltrim( $release_data['tag_name'] ?? '', 'v' );
|
||||||
|
|
||||||
|
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 = preg_replace( '#^https?://#', '', $url );
|
||||||
|
|
||||||
|
// 移除 gitee.com
|
||||||
|
$url = preg_replace( '#^gitee\.com/#', '', $url );
|
||||||
|
|
||||||
|
// 移除 .git 后缀
|
||||||
|
$url = preg_replace( '#\.git$#', '', $url );
|
||||||
|
|
||||||
|
// 验证格式
|
||||||
|
if ( preg_match( '#^[\w.-]+/[\w.-]+$#', $url ) ) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
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'] ?? '';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用归档 URL 作为后备
|
||||||
|
$tag = $release['tag_name'] ?? '';
|
||||||
|
if ( ! empty( $tag ) ) {
|
||||||
|
return 'https://gitee.com/' . $repo . '/repository/archive/' . $tag . '.zip';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
339
includes/UpdateSource/Handlers/HandlerInterface.php
Normal file
339
includes/UpdateSource/Handlers/HandlerInterface.php
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 处理器接口
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\UpdateSource\SourceModel;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新源处理器接口
|
||||||
|
*/
|
||||||
|
interface HandlerInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param SourceModel $source 源模型
|
||||||
|
*/
|
||||||
|
public function __construct( SourceModel $source );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_capabilities(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检查 URL
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_check_url(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求头
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function get_info( string $slug ): ?array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证认证信息
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function validate_auth(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连通性
|
||||||
|
*
|
||||||
|
* @return HealthStatus
|
||||||
|
*/
|
||||||
|
public function test_connection(): HealthStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新信息类
|
||||||
|
*/
|
||||||
|
class UpdateInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件/主题 slug
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $slug = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新版本号
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $version = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载 URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $download_url = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详情 URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $details_url = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最低 WordPress 版本
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $requires = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试通过的 WordPress 版本
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $tested = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最低 PHP 版本
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $requires_php = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后更新时间
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $last_updated = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图标
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public array $icons = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 横幅
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public array $banners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新日志
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $changelog = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $description = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数组创建
|
||||||
|
*
|
||||||
|
* @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'] ?? [];
|
||||||
|
$info->banners = $data['banners'] ?? [];
|
||||||
|
$info->changelog = $data['changelog'] ?? $data['sections']['changelog'] ?? '';
|
||||||
|
$info->description = $data['description'] ?? $data['sections']['description'] ?? '';
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为 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) [
|
||||||
|
'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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康状态类
|
||||||
|
*/
|
||||||
|
class HealthStatus {
|
||||||
|
|
||||||
|
const STATUS_HEALTHY = 'healthy';
|
||||||
|
const STATUS_DEGRADED = 'degraded';
|
||||||
|
const STATUS_FAILED = 'failed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $status = self::STATUS_FAILED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应时间(毫秒)
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public int $response_time = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误信息
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $error = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查时间
|
||||||
|
*
|
||||||
|
* @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 响应时间
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否健康
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
includes/UpdateSource/Handlers/HealthStatus.php
Normal file
15
includes/UpdateSource/Handlers/HealthStatus.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* HealthStatus 兼容加载文件
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/HandlerInterface.php';
|
||||||
141
includes/UpdateSource/Handlers/JsonHandler.php
Normal file
141
includes/UpdateSource/Handlers/JsonHandler.php
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* JSON API 处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON API 处理器
|
||||||
|
* 兼容 Plugin Update Checker 格式
|
||||||
|
*/
|
||||||
|
class JsonHandler extends AbstractHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查更新
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 Plugin Update Checker 格式
|
||||||
|
$remote_version = $data['version'] ?? '';
|
||||||
|
|
||||||
|
if ( empty( $remote_version ) ) {
|
||||||
|
Logger::warning( 'JSON 响应缺少版本信息', [ 'url' => $url ] );
|
||||||
|
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;
|
||||||
|
|
||||||
|
Logger::info( '发现更新', [
|
||||||
|
'slug' => $slug,
|
||||||
|
'current' => $version,
|
||||||
|
'new' => $remote_version,
|
||||||
|
] );
|
||||||
|
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建检查 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, '?' ) !== false ) {
|
||||||
|
return add_query_arg( 'slug', $slug, $url );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
includes/UpdateSource/Handlers/PUCHandler.php
Normal file
20
includes/UpdateSource/Handlers/PUCHandler.php
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* PUC 处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin Update Checker 处理器
|
||||||
|
* 复用 JSON 处理逻辑
|
||||||
|
*/
|
||||||
|
class PUCHandler extends JsonHandler {
|
||||||
|
}
|
||||||
15
includes/UpdateSource/Handlers/UpdateInfo.php
Normal file
15
includes/UpdateSource/Handlers/UpdateInfo.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* UpdateInfo 兼容加载文件
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/HandlerInterface.php';
|
||||||
300
includes/UpdateSource/Handlers/WenPaiGitHandler.php
Normal file
300
includes/UpdateSource/Handlers/WenPaiGitHandler.php
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 菲码源库处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 菲码源库处理器(Gitea API)
|
||||||
|
*/
|
||||||
|
class WenPaiGitHandler extends AbstractHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 基础 URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const API_BASE = 'https://git.wenpai.org/api/v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检查 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 );
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
if ( null === $data || empty( $data ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$latest = $data[0] ?? null;
|
||||||
|
if ( null === $latest ) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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'] ?? '';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$latest = $release_data[0] ?? [];
|
||||||
|
$version = ltrim( $latest['tag_name'] ?? '', 'v' );
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
$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( '#^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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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( '#^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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找下载 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = $release['tag_name'] ?? '';
|
||||||
|
if ( ! empty( $tag ) ) {
|
||||||
|
return $this->get_api_base() . '/repos/' . $repo . '/archive/' . $tag . '.zip';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '';
|
||||||
|
|
||||||
|
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 self::API_BASE;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
includes/UpdateSource/Handlers/ZipHandler.php
Normal file
116
includes/UpdateSource/Handlers/ZipHandler.php
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ZIP 处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource\Handlers;
|
||||||
|
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZIP 处理器(直接下载地址)
|
||||||
|
*
|
||||||
|
* 需要 metadata 中提供 version/new_version,或从 URL 文件名推断版本
|
||||||
|
*/
|
||||||
|
class ZipHandler extends AbstractHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取能力列表
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
if ( empty( $remote_version ) ) {
|
||||||
|
Logger::debug( 'ZIP: 无法解析版本号', [ 'url' => $this->source->api_url ] );
|
||||||
|
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;
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目信息
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ?? [];
|
||||||
|
|
||||||
|
if ( ! empty( $metadata['version'] ) ) {
|
||||||
|
return (string) $metadata['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 '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = basename( $path );
|
||||||
|
if ( preg_match( '/(\d+\.\d+\.\d+(?:[-+][\w\.]+)?)/', $filename, $matches ) ) {
|
||||||
|
return $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
360
includes/UpdateSource/PluginUpdater.php
Normal file
360
includes/UpdateSource/PluginUpdater.php
Normal file
|
|
@ -0,0 +1,360 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 插件更新器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\UpdateSource\Handlers\UpdateInfo;
|
||||||
|
use WPBridge\Core\ItemSourceManager;
|
||||||
|
use WPBridge\Cache\FallbackStrategy;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件更新器类
|
||||||
|
*/
|
||||||
|
class PluginUpdater {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源解析器(方案 B)
|
||||||
|
*
|
||||||
|
* @var SourceResolver
|
||||||
|
*/
|
||||||
|
private SourceResolver $source_resolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 降级策略
|
||||||
|
*
|
||||||
|
* @var FallbackStrategy
|
||||||
|
*/
|
||||||
|
private FallbackStrategy $fallback_strategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存键前缀
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks(): void {
|
||||||
|
// 插件更新检查
|
||||||
|
add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_updates' ], 10, 1 );
|
||||||
|
|
||||||
|
// 插件信息
|
||||||
|
add_filter( 'plugins_api', [ $this, 'plugin_info' ], 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! isset( $transient->response ) ) {
|
||||||
|
$transient->response = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! isset( $transient->no_update ) ) {
|
||||||
|
$transient->no_update = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取已安装的插件
|
||||||
|
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 );
|
||||||
|
|
||||||
|
$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) [
|
||||||
|
'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;
|
||||||
|
|
||||||
|
if ( $take_over ) {
|
||||||
|
// 接管更新检查,清除默认响应
|
||||||
|
unset( $transient->response[ $plugin_file ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从缓存获取(使用 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 ] );
|
||||||
|
} 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 );
|
||||||
|
|
||||||
|
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 ] );
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
set_transient( $cache_key, [
|
||||||
|
'update' => (array) $update_object,
|
||||||
|
], $this->settings->get_cache_ttl() );
|
||||||
|
|
||||||
|
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, [
|
||||||
|
'update' => null,
|
||||||
|
], $this->settings->get_cache_ttl() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() );
|
||||||
|
|
||||||
|
$result = $this->fallback_strategy->execute_with_fallback(
|
||||||
|
$sources,
|
||||||
|
function( SourceModel $source ) use ( $slug, $version ) {
|
||||||
|
$handler = $source->get_handler();
|
||||||
|
|
||||||
|
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( '检查更新时发生错误', [
|
||||||
|
'source' => $source->id,
|
||||||
|
'slug' => $slug,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
] );
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
$cache_key
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = $args->slug ?? '';
|
||||||
|
|
||||||
|
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'];
|
||||||
|
|
||||||
|
if ( $mode === ItemSourceManager::MODE_DISABLED || empty( $sources ) ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取第一个匹配的源
|
||||||
|
$source = reset( $sources );
|
||||||
|
$handler = $source->get_handler();
|
||||||
|
|
||||||
|
if ( null === $handler ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = $handler->get_info( $slug );
|
||||||
|
|
||||||
|
if ( null === $info ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 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';
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤下载
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除插件更新缓存
|
||||||
|
*
|
||||||
|
* @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' );
|
||||||
|
}
|
||||||
|
}
|
||||||
126
includes/UpdateSource/PresetSources.php
Normal file
126
includes/UpdateSource/PresetSources.php
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 预置更新源配置
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预置更新源配置类
|
||||||
|
*/
|
||||||
|
class PresetSources {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文派开源更新源(默认预置)
|
||||||
|
*/
|
||||||
|
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 = [
|
||||||
|
'id' => 'arkpress',
|
||||||
|
'name' => 'ArkPress',
|
||||||
|
'type' => SourceType::ARKPRESS,
|
||||||
|
'api_url' => '', // 用户自定义
|
||||||
|
'enabled' => false,
|
||||||
|
'priority' => 20,
|
||||||
|
'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 = [
|
||||||
|
'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 [
|
||||||
|
self::WENPAI_OPEN,
|
||||||
|
// 以下预置源默认不添加,用户可手动启用
|
||||||
|
// self::ARKPRESS,
|
||||||
|
// self::ASPIRECLOUD,
|
||||||
|
// 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 = [
|
||||||
|
'wenpai-open' => self::WENPAI_OPEN,
|
||||||
|
'arkpress' => self::ARKPRESS,
|
||||||
|
'aspirecloud' => self::ASPIRECLOUD,
|
||||||
|
'fair' => self::FAIR,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $all[ $id ] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否是预置源 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
254
includes/UpdateSource/SourceManager.php
Normal file
254
includes/UpdateSource/SourceManager.php
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 更新源管理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新源管理器类
|
||||||
|
*/
|
||||||
|
class SourceManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存的源模型
|
||||||
|
*
|
||||||
|
* @var array<string, SourceModel>
|
||||||
|
*/
|
||||||
|
private array $source_models = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*
|
||||||
|
* @param Settings $settings 设置实例
|
||||||
|
*/
|
||||||
|
public function __construct( Settings $settings ) {
|
||||||
|
$this->settings = $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有源
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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_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 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = $this->settings->get_source( $id );
|
||||||
|
if ( null === $source ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加源
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->settings->add_source( $source->to_array() );
|
||||||
|
|
||||||
|
if ( $result ) {
|
||||||
|
$this->source_models[ $source->id ] = $source;
|
||||||
|
Logger::info( '添加源成功', [ 'id' => $source->id, 'name' => $source->name ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新源
|
||||||
|
*
|
||||||
|
* @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() );
|
||||||
|
|
||||||
|
if ( $result ) {
|
||||||
|
$this->source_models[ $source->id ] = $source;
|
||||||
|
Logger::info( '更新源成功', [ 'id' => $source->id ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除源
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
|
||||||
|
if ( $result ) {
|
||||||
|
unset( $this->source_models[ $id ] );
|
||||||
|
Logger::info( '删除源成功', [ 'id' => $id ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
|
||||||
|
if ( $result && isset( $this->source_models[ $id ] ) ) {
|
||||||
|
$this->source_models[ $id ]->enabled = $enabled;
|
||||||
|
Logger::info( $enabled ? '启用源' : '禁用源', [ 'id' => $id ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取源统计
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_stats(): array {
|
||||||
|
$all = $this->get_all();
|
||||||
|
$enabled = $this->get_enabled();
|
||||||
|
|
||||||
|
$by_type = [];
|
||||||
|
foreach ( $all as $source ) {
|
||||||
|
$type = $source->type;
|
||||||
|
if ( ! isset( $by_type[ $type ] ) ) {
|
||||||
|
$by_type[ $type ] = 0;
|
||||||
|
}
|
||||||
|
$by_type[ $type ]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => count( $all ),
|
||||||
|
'enabled' => count( $enabled ),
|
||||||
|
'by_type' => $by_type,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存
|
||||||
|
*/
|
||||||
|
public function clear_cache(): void {
|
||||||
|
$this->source_models = [];
|
||||||
|
$this->settings->clear_cache();
|
||||||
|
}
|
||||||
|
}
|
||||||
271
includes/UpdateSource/SourceModel.php
Normal file
271
includes/UpdateSource/SourceModel.php
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 更新源数据模型
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource;
|
||||||
|
|
||||||
|
use WPBridge\Security\Encryption;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新源模型类
|
||||||
|
*/
|
||||||
|
class SourceModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 唯一标识
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $id = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源名称
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $name = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源类型(见 SourceType 枚举)
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $type = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API URL
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $api_url = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件/主题 slug
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $slug = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目类型:plugin 或 theme
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $item_type = 'plugin';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证令牌
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $auth_token = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git 分支(可选)
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public string $branch = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public bool $enabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级(数字越小优先级越高)
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public int $priority = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是预置源
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public bool $is_preset = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是内联源(项目专属,通过快速设置创建)
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public bool $is_inline = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 额外元数据
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public array $metadata = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数组创建实例
|
||||||
|
*
|
||||||
|
* @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'] ?? [];
|
||||||
|
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为数组
|
||||||
|
*
|
||||||
|
* @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 = [];
|
||||||
|
|
||||||
|
// 验证类型
|
||||||
|
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, [ 'http', 'https' ], true ) ) {
|
||||||
|
$errors['api_url'] = __( 'URL 必须使用 http 或 https 协议', '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' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有效
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
|
||||||
|
if ( null === $handler_class || ! class_exists( $handler_class ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求头
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_headers(): array {
|
||||||
|
$headers = [];
|
||||||
|
|
||||||
|
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 解密失败', [ '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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
237
includes/UpdateSource/SourceResolver.php
Normal file
237
includes/UpdateSource/SourceResolver.php
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 更新源解析器
|
||||||
|
*
|
||||||
|
* 连接项目配置(方案 B)与更新处理器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource;
|
||||||
|
|
||||||
|
use WPBridge\Core\DefaultsManager;
|
||||||
|
use WPBridge\Core\ItemSourceManager;
|
||||||
|
use WPBridge\Core\SourceRegistry;
|
||||||
|
use WPBridge\Security\Encryption;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新源解析器类
|
||||||
|
*/
|
||||||
|
class SourceResolver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源注册表
|
||||||
|
*
|
||||||
|
* @var SourceRegistry
|
||||||
|
*/
|
||||||
|
private SourceRegistry $source_registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目配置管理器
|
||||||
|
*
|
||||||
|
* @var ItemSourceManager
|
||||||
|
*/
|
||||||
|
private ItemSourceManager $item_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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析指定项目的更新源
|
||||||
|
*
|
||||||
|
* @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 [
|
||||||
|
'mode' => $mode,
|
||||||
|
'sources' => [],
|
||||||
|
'has_wporg' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sources = $this->item_manager->get_effective_sources( $item_key, $this->defaults_manager );
|
||||||
|
|
||||||
|
if ( empty( $sources ) ) {
|
||||||
|
return [
|
||||||
|
'mode' => $mode,
|
||||||
|
'sources' => [],
|
||||||
|
'has_wporg' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射源类型
|
||||||
|
*
|
||||||
|
* @param array $source 源配置
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
private function map_type( array $source ): ?string {
|
||||||
|
$type = $source['type'] ?? '';
|
||||||
|
|
||||||
|
switch ( $type ) {
|
||||||
|
case SourceRegistry::TYPE_WPORG:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case SourceRegistry::TYPE_MIRROR:
|
||||||
|
return SourceType::ARKPRESS;
|
||||||
|
|
||||||
|
case SourceRegistry::TYPE_FAIR:
|
||||||
|
return SourceType::FAIR;
|
||||||
|
|
||||||
|
case SourceRegistry::TYPE_JSON:
|
||||||
|
return SourceType::JSON;
|
||||||
|
|
||||||
|
case SourceRegistry::TYPE_ARKPRESS:
|
||||||
|
return SourceType::ARKPRESS;
|
||||||
|
|
||||||
|
case SourceRegistry::TYPE_GIT:
|
||||||
|
return $this->resolve_git_type( $source['api_url'] ?? '' );
|
||||||
|
|
||||||
|
case SourceRegistry::TYPE_CUSTOM:
|
||||||
|
return $this->guess_custom_type( $source['api_url'] ?? '' );
|
||||||
|
|
||||||
|
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 ) );
|
||||||
|
|
||||||
|
if ( strpos( $host, 'github.com' ) !== false ) {
|
||||||
|
return SourceType::GITHUB;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( strpos( $host, 'gitlab' ) !== false ) {
|
||||||
|
return SourceType::GITLAB;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( strpos( $host, 'gitee.com' ) !== false ) {
|
||||||
|
return SourceType::GITEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( strpos( $host, 'wenpai' ) !== false || strpos( $host, 'feicode' ) !== false ) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SourceType::JSON;
|
||||||
|
}
|
||||||
|
}
|
||||||
205
includes/UpdateSource/SourceType.php
Normal file
205
includes/UpdateSource/SourceType.php
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 源类型枚举
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新源类型枚举
|
||||||
|
* 所有源类型的统一定义,确保数据模型与处理器一致
|
||||||
|
*/
|
||||||
|
class SourceType {
|
||||||
|
|
||||||
|
// === 基础类型(用户自定义源)===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准 JSON API(Plugin Update Checker 格式)
|
||||||
|
*/
|
||||||
|
const JSON = 'json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub Releases
|
||||||
|
*/
|
||||||
|
const GITHUB = 'github';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitLab Releases
|
||||||
|
*/
|
||||||
|
const GITLAB = 'gitlab';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gitee Releases(国内)
|
||||||
|
*/
|
||||||
|
const GITEE = 'gitee';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 菲码源库
|
||||||
|
*/
|
||||||
|
const WENPAI_GIT = 'wenpai_git';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接 ZIP URL
|
||||||
|
*/
|
||||||
|
const ZIP = 'zip';
|
||||||
|
|
||||||
|
// === 自托管服务器类型(预置源使用)===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArkPress(文派自托管,AspireCloud 分叉)
|
||||||
|
*/
|
||||||
|
const ARKPRESS = 'arkpress';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AspireCloud
|
||||||
|
*/
|
||||||
|
const ASPIRECLOUD = 'aspirecloud';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FAIR Package Manager
|
||||||
|
*/
|
||||||
|
const FAIR = 'fair';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin Update Checker 服务器
|
||||||
|
*/
|
||||||
|
const PUC = 'puc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WPBridge Server(商业插件桥接服务)
|
||||||
|
*/
|
||||||
|
const BRIDGE_SERVER = 'bridge_server';
|
||||||
|
|
||||||
|
// === 类型分组 ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git 平台类型
|
||||||
|
*/
|
||||||
|
const GIT_TYPES = [
|
||||||
|
self::GITHUB,
|
||||||
|
self::GITLAB,
|
||||||
|
self::GITEE,
|
||||||
|
self::WENPAI_GIT,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自托管服务器类型
|
||||||
|
*/
|
||||||
|
const SERVER_TYPES = [
|
||||||
|
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 [
|
||||||
|
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 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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否是服务器类型
|
||||||
|
*
|
||||||
|
* @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 = [
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
317
includes/UpdateSource/ThemeUpdater.php
Normal file
317
includes/UpdateSource/ThemeUpdater.php
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 主题更新器
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WPBridge\UpdateSource;
|
||||||
|
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\UpdateSource\Handlers\UpdateInfo;
|
||||||
|
use WPBridge\Core\ItemSourceManager;
|
||||||
|
use WPBridge\Cache\FallbackStrategy;
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题更新器类
|
||||||
|
*/
|
||||||
|
class ThemeUpdater {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实例
|
||||||
|
*
|
||||||
|
* @var Settings
|
||||||
|
*/
|
||||||
|
private Settings $settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源解析器(方案 B)
|
||||||
|
*
|
||||||
|
* @var SourceResolver
|
||||||
|
*/
|
||||||
|
private SourceResolver $source_resolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 降级策略
|
||||||
|
*
|
||||||
|
* @var FallbackStrategy
|
||||||
|
*/
|
||||||
|
private FallbackStrategy $fallback_strategy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存键前缀
|
||||||
|
*
|
||||||
|
* @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 );
|
||||||
|
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化钩子
|
||||||
|
*/
|
||||||
|
private function init_hooks(): void {
|
||||||
|
// 主题更新检查
|
||||||
|
add_filter( 'pre_set_site_transient_update_themes', [ $this, 'check_updates' ], 10, 1 );
|
||||||
|
|
||||||
|
// 主题信息
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! isset( $transient->response ) ) {
|
||||||
|
$transient->response = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! isset( $transient->no_update ) ) {
|
||||||
|
$transient->no_update = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取已安装的主题
|
||||||
|
$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'] );
|
||||||
|
|
||||||
|
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 ] = [
|
||||||
|
'theme' => $slug,
|
||||||
|
'new_version' => $theme->get( 'Version' ),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$take_over = ( $mode === ItemSourceManager::MODE_CUSTOM ) || ! $allow_wporg_fallback;
|
||||||
|
|
||||||
|
if ( $take_over ) {
|
||||||
|
// 接管更新检查,清除默认响应
|
||||||
|
unset( $transient->response[ $slug ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$version = $theme->get( 'Version' );
|
||||||
|
|
||||||
|
// 尝试从缓存获取(使用 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 ] );
|
||||||
|
} else {
|
||||||
|
if ( $take_over ) {
|
||||||
|
$transient->no_update[ $slug ] = [
|
||||||
|
'theme' => $slug,
|
||||||
|
'new_version' => $version,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查更新
|
||||||
|
$update_info = $this->check_theme_update( $slug, $version, $matching_sources );
|
||||||
|
|
||||||
|
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 ] );
|
||||||
|
|
||||||
|
// 缓存结果
|
||||||
|
set_transient( $cache_key, [
|
||||||
|
'update' => $update_data,
|
||||||
|
], $this->settings->get_cache_ttl() );
|
||||||
|
|
||||||
|
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, [
|
||||||
|
'update' => null,
|
||||||
|
], $this->settings->get_cache_ttl() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() );
|
||||||
|
|
||||||
|
$result = $this->fallback_strategy->execute_with_fallback(
|
||||||
|
$sources,
|
||||||
|
function( SourceModel $source ) use ( $slug, $version ) {
|
||||||
|
$handler = $source->get_handler();
|
||||||
|
|
||||||
|
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( '检查主题更新时发生错误', [
|
||||||
|
'source' => $source->id,
|
||||||
|
'slug' => $slug,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
] );
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
$cache_key
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = $args->slug ?? '';
|
||||||
|
|
||||||
|
if ( empty( $slug ) ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取第一个匹配的源
|
||||||
|
$source = reset( $sources );
|
||||||
|
$handler = $source->get_handler();
|
||||||
|
|
||||||
|
if ( null === $handler ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = $handler->get_info( $slug );
|
||||||
|
|
||||||
|
if ( null === $info ) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 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 ) . '%'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除 WordPress 更新缓存
|
||||||
|
delete_site_transient( 'update_themes' );
|
||||||
|
}
|
||||||
|
}
|
||||||
355
languages/wpbridge.pot
Normal file
355
languages/wpbridge.pot
Normal file
|
|
@ -0,0 +1,355 @@
|
||||||
|
# WPBridge Translation Template
|
||||||
|
# Copyright (C) 2026 WenPai.org
|
||||||
|
# This file is distributed under the GPL-2.0-or-later.
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: WPBridge 0.1.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: https://wenpai.org/\n"
|
||||||
|
"POT-Creation-Date: 2026-02-04 00:00:00+0000\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: wpbridge.php
|
||||||
|
msgid "WPBridge"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "更新源"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "设置"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "安全检查失败"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "权限不足"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "更新源已添加"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "更新源已更新"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "保存失败"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "无效的源 ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "更新源已删除"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "删除失败,可能是预置源"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "设置已保存"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "状态已更新"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "更新失败"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "源不存在"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/AdminPage.php
|
||||||
|
msgid "缓存已清除"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "WPBridge 更新源"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "添加更新源"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "总数"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "已启用"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "清除缓存"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "状态"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "名称"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "类型"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "Slug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "优先级"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "操作"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "暂无更新源"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "预置"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "全部"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "测试"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "编辑"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-list.php
|
||||||
|
msgid "删除"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "编辑更新源"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "添加更新源"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "更新源的显示名称"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "更新源的类型"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "API URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "更新源的 API 地址。对于 JSON 类型,可以使用 {slug} 占位符。"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "插件/主题的 slug。留空表示匹配所有。"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "项目类型"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "插件"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "主题"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "认证令牌"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "用于私有仓库或需要认证的 API。留空表示无需认证。"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "数字越小优先级越高(0-100)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "启用"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "启用此更新源"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "更新"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "添加"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/source-editor.php
|
||||||
|
msgid "取消"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "WPBridge 设置"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "常规设置"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "调试模式"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "启用调试日志"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "启用后会记录详细的调试信息,仅在排查问题时启用。"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "缓存时间"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "1 小时"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "6 小时"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "12 小时"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "24 小时"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "更新检查结果的缓存时间"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "请求超时"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "秒"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "HTTP 请求的超时时间(5-60 秒)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "降级策略"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "启用过期缓存兜底"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "当更新源不可用时,使用过期的缓存数据。"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "保存设置"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "调试日志"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "暂无日志"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "时间"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "级别"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/admin/settings.php
|
||||||
|
msgid "消息"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/UpdateSource/SourceType.php
|
||||||
|
msgid "JSON API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/UpdateSource/SourceType.php
|
||||||
|
msgid "GitHub"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/UpdateSource/SourceType.php
|
||||||
|
msgid "GitLab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/UpdateSource/SourceType.php
|
||||||
|
msgid "Gitee"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/UpdateSource/SourceType.php
|
||||||
|
msgid "菲码源库"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/UpdateSource/SourceType.php
|
||||||
|
msgid "ZIP URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/UpdateSource/SourceType.php
|
||||||
|
msgid "ArkPress"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/UpdateSource/SourceType.php
|
||||||
|
msgid "AspireCloud"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/UpdateSource/SourceType.php
|
||||||
|
msgid "FAIR"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/UpdateSource/SourceType.php
|
||||||
|
msgid "PUC Server"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Core/Settings.php
|
||||||
|
msgid "文派开源更新源"
|
||||||
|
msgstr ""
|
||||||
144
templates/admin/main.php
Normal file
144
templates/admin/main.php
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WPBridge 主设置页面模板
|
||||||
|
*
|
||||||
|
* 参考 WPMind Gutenberg 风格设计
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.5.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use WPBridge\UpdateSource\SourceType;
|
||||||
|
use WPBridge\UpdateSource\SourceManager;
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
use WPBridge\Core\Logger;
|
||||||
|
use WPBridge\Cache\HealthChecker;
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
$settings_obj = new Settings();
|
||||||
|
$source_manager = new SourceManager( $settings_obj );
|
||||||
|
$sources = $source_manager->get_all();
|
||||||
|
$stats = $source_manager->get_stats();
|
||||||
|
$settings = $settings_obj->get_all();
|
||||||
|
$logs = Logger::get_logs();
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
$health_checker = new HealthChecker( $settings_obj );
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!-- 标题栏 -->
|
||||||
|
<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-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>
|
||||||
|
|
||||||
|
<!-- 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 include WPBRIDGE_PATH . 'templates/admin/tabs/projects.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 include WPBRIDGE_PATH . 'templates/admin/tabs/vendors.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 include WPBRIDGE_PATH . 'templates/admin/tabs/settings.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 include WPBRIDGE_PATH . 'templates/admin/tabs/logs.php'; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
138
templates/admin/partials/defaults-config.php
Normal file
138
templates/admin/partials/defaults-config.php
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 默认规则配置部分模板
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.6.0
|
||||||
|
* @var array $all_sources 所有可用源
|
||||||
|
* @var SourceRegistry $source_registry 源注册表
|
||||||
|
* @var DefaultsManager $defaults_manager 默认规则管理器
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use WPBridge\Core\DefaultsManager;
|
||||||
|
|
||||||
|
// 获取当前默认规则
|
||||||
|
$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>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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-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>
|
||||||
284
templates/admin/partials/project-list-plugins.php
Normal file
284
templates/admin/partials/project-list-plugins.php
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 插件列表部分模板
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.6.0
|
||||||
|
* @var array $installed_plugins 已安装插件
|
||||||
|
* @var array $all_sources 所有可用源
|
||||||
|
* @var ItemSourceManager $item_manager 项目配置管理器
|
||||||
|
* @var DefaultsManager $defaults_manager 默认规则管理器
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use WPBridge\Core\ItemSourceManager;
|
||||||
|
use WPBridge\Core\CommercialDetector;
|
||||||
|
use WPBridge\Core\VersionLock;
|
||||||
|
|
||||||
|
// 获取商业插件检测器
|
||||||
|
$commercial_detector = CommercialDetector::get_instance();
|
||||||
|
// 获取版本锁定管理器
|
||||||
|
$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>
|
||||||
|
|
||||||
|
<!-- 插件列表 -->
|
||||||
|
<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 );
|
||||||
|
|
||||||
|
// 获取插件 slug
|
||||||
|
$plugin_slug = dirname( $plugin_file );
|
||||||
|
if ( $plugin_slug === '.' ) {
|
||||||
|
$plugin_slug = basename( $plugin_file, '.php' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否激活
|
||||||
|
$is_active = is_plugin_active( $plugin_file );
|
||||||
|
|
||||||
|
// 获取版本锁定信息
|
||||||
|
$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>
|
||||||
|
|
||||||
|
<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-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>
|
||||||
188
templates/admin/partials/project-list-themes.php
Normal file
188
templates/admin/partials/project-list-themes.php
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 主题列表部分模板
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.6.0
|
||||||
|
* @var array $installed_themes 已安装主题
|
||||||
|
* @var array $all_sources 所有可用源
|
||||||
|
* @var ItemSourceManager $item_manager 项目配置管理器
|
||||||
|
* @var DefaultsManager $defaults_manager 默认规则管理器
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use WPBridge\Core\ItemSourceManager;
|
||||||
|
|
||||||
|
// 获取当前主题
|
||||||
|
$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>
|
||||||
|
|
||||||
|
<!-- 主题列表 -->
|
||||||
|
<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;
|
||||||
|
|
||||||
|
// 判断是否当前主题
|
||||||
|
$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>
|
||||||
|
|
||||||
|
<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-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>
|
||||||
145
templates/admin/settings.php
Normal file
145
templates/admin/settings.php
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 设置页面模板
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @var array $settings 设置
|
||||||
|
* @var array $logs 日志
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap wpbridge-wrap">
|
||||||
|
<h1><?php esc_html_e( 'WPBridge 设置', 'wpbridge' ); ?></h1>
|
||||||
|
|
||||||
|
<?php settings_errors( 'wpbridge' ); ?>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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-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>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<input type="submit" class="button button-primary" value="<?php esc_attr_e( '保存设置', 'wpbridge' ); ?>">
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<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; ?>
|
||||||
|
</div>
|
||||||
213
templates/admin/source-editor.php
Normal file
213
templates/admin/source-editor.php
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 更新源编辑模板
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.5.0
|
||||||
|
* @var \WPBridge\UpdateSource\SourceModel|null $source 源模型
|
||||||
|
* @var array $types 类型列表
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_edit = null !== $source;
|
||||||
|
$title = $is_edit ? __( '编辑更新源', 'wpbridge' ) : __( '添加更新源', '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>
|
||||||
|
</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' ); ?>
|
||||||
|
|
||||||
|
<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-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>
|
||||||
|
<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-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>
|
||||||
|
<?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-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-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>
|
||||||
129
templates/admin/source-list.php
Normal file
129
templates/admin/source-list.php
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 更新源列表模板
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @var array $sources 源列表
|
||||||
|
* @var array $stats 统计信息
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
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">
|
||||||
|
|
||||||
|
<?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>
|
||||||
|
|
||||||
|
<!-- 源列表 -->
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
190
templates/admin/tabs/api.php
Normal file
190
templates/admin/tabs/api.php
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bridge API Tab 内容
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.5.0
|
||||||
|
* @var array $settings 设置
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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'] ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
|
||||||
|
<!-- 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-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-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>
|
||||||
|
|
||||||
|
<?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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
359
templates/admin/tabs/diagnostics.php
Normal file
359
templates/admin/tabs/diagnostics.php
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 诊断工具 Tab 内容
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.7.0
|
||||||
|
* @var array $sources 源列表
|
||||||
|
* @var array $stats 统计信息
|
||||||
|
* @var array $settings 设置
|
||||||
|
* @var array $health_status 健康状态
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- 诊断结果概览 -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 更新源连通性测试 -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<?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-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
|
||||||
|
// 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
|
||||||
|
// 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
|
||||||
|
// 内存限制检查
|
||||||
|
$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>
|
||||||
|
</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-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
|
||||||
|
// 请求超时检查
|
||||||
|
$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
|
||||||
|
// 启用的更新源数量检查
|
||||||
|
$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>
|
||||||
60
templates/admin/tabs/logs.php
Normal file
60
templates/admin/tabs/logs.php
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 日志 Tab 内容
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.5.0
|
||||||
|
* @var array $logs 日志列表
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
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>
|
||||||
|
|
||||||
|
<?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' ); ?>
|
||||||
|
</div>
|
||||||
269
templates/admin/tabs/overview.php
Normal file
269
templates/admin/tabs/overview.php
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 概览 Tab 内容 - 状态仪表板
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.7.0
|
||||||
|
* @var array $sources 源列表
|
||||||
|
* @var array $stats 统计信息
|
||||||
|
* @var array $settings 设置
|
||||||
|
* @var array $health_status 健康状态
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
$plugins_count = count( get_plugins() );
|
||||||
|
$themes_count = count( wp_get_themes() );
|
||||||
|
|
||||||
|
// 获取项目配置统计
|
||||||
|
$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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 健康源统计
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存状态
|
||||||
|
$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;
|
||||||
|
$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>
|
||||||
|
|
||||||
|
<!-- 核心指标 -->
|
||||||
|
<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-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 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-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>
|
||||||
77
templates/admin/tabs/projects.php
Normal file
77
templates/admin/tabs/projects.php
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 项目管理 Tab 内容
|
||||||
|
*
|
||||||
|
* 方案 B:项目优先架构 - 显示已安装的插件/主题列表
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.6.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use WPBridge\Core\SourceRegistry;
|
||||||
|
use WPBridge\Core\ItemSourceManager;
|
||||||
|
use WPBridge\Core\DefaultsManager;
|
||||||
|
|
||||||
|
// 获取管理器实例
|
||||||
|
$source_registry = new SourceRegistry();
|
||||||
|
$item_manager = new ItemSourceManager( $source_registry );
|
||||||
|
$defaults_manager = new DefaultsManager();
|
||||||
|
|
||||||
|
// 获取所有可用源
|
||||||
|
$all_sources = $source_registry->get_enabled();
|
||||||
|
|
||||||
|
// 获取已安装的插件
|
||||||
|
if ( ! function_exists( 'get_plugins' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
$installed_plugins = get_plugins();
|
||||||
|
|
||||||
|
// 获取已安装的主题
|
||||||
|
$installed_themes = wp_get_themes();
|
||||||
|
|
||||||
|
// 当前子 Tab - 白名单验证
|
||||||
|
$allowed_subtabs = [ 'plugins', 'themes', 'defaults' ];
|
||||||
|
$current_subtab = 'plugins';
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- 仅用于 UI 显示
|
||||||
|
if ( isset( $_GET['subtab'] ) && in_array( $_GET['subtab'], $allowed_subtabs, true ) ) {
|
||||||
|
$current_subtab = $_GET['subtab'];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- 子 Tab 导航 -->
|
||||||
|
<div class="wpbridge-subtabs">
|
||||||
|
<a href="#" class="wpbridge-subtab <?php echo $current_subtab === 'plugins' ? 'wpbridge-subtab-active' : ''; ?>" data-subtab="plugins">
|
||||||
|
<span class="dashicons dashicons-admin-plugins"></span>
|
||||||
|
<?php esc_html_e( '插件', 'wpbridge' ); ?>
|
||||||
|
<span class="wpbridge-subtab-count"><?php echo count( $installed_plugins ); ?></span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="wpbridge-subtab <?php echo $current_subtab === 'themes' ? 'wpbridge-subtab-active' : ''; ?>" data-subtab="themes">
|
||||||
|
<span class="dashicons dashicons-admin-appearance"></span>
|
||||||
|
<?php esc_html_e( '主题', 'wpbridge' ); ?>
|
||||||
|
<span class="wpbridge-subtab-count"><?php echo count( $installed_themes ); ?></span>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="wpbridge-subtab <?php echo $current_subtab === 'defaults' ? 'wpbridge-subtab-active' : ''; ?>" data-subtab="defaults">
|
||||||
|
<span class="dashicons dashicons-admin-settings"></span>
|
||||||
|
<?php esc_html_e( '默认规则', 'wpbridge' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 插件列表 -->
|
||||||
|
<div id="subtab-plugins" class="wpbridge-subtab-pane <?php echo $current_subtab === 'plugins' ? 'wpbridge-subtab-pane-active' : ''; ?>">
|
||||||
|
<?php include WPBRIDGE_PATH . 'templates/admin/partials/project-list-plugins.php'; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主题列表 -->
|
||||||
|
<div id="subtab-themes" class="wpbridge-subtab-pane <?php echo $current_subtab === 'themes' ? 'wpbridge-subtab-pane-active' : ''; ?>">
|
||||||
|
<?php include WPBRIDGE_PATH . 'templates/admin/partials/project-list-themes.php'; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 默认规则 -->
|
||||||
|
<div id="subtab-defaults" class="wpbridge-subtab-pane <?php echo $current_subtab === 'defaults' ? 'wpbridge-subtab-pane-active' : ''; ?>">
|
||||||
|
<?php include WPBRIDGE_PATH . 'templates/admin/partials/defaults-config.php'; ?>
|
||||||
|
</div>
|
||||||
194
templates/admin/tabs/settings.php
Normal file
194
templates/admin/tabs/settings.php
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 设置 Tab 内容
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.5.0
|
||||||
|
* @var array $settings 设置
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<form method="post" class="wpbridge-settings-form">
|
||||||
|
<?php wp_nonce_field( 'wpbridge_action', 'wpbridge_nonce' ); ?>
|
||||||
|
<input type="hidden" name="wpbridge_action" value="save_settings">
|
||||||
|
|
||||||
|
<div class="wpbridge-settings-panel">
|
||||||
|
<!-- 调试模式 -->
|
||||||
|
<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( '启用后会记录详细的调试信息,仅在排查问题时启用。', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<label class="wpbridge-toggle">
|
||||||
|
<input type="checkbox" name="debug_mode" value="1" <?php checked( $settings['debug_mode'] ?? false ); ?>>
|
||||||
|
<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( '更新检查结果的缓存时间,较长的缓存时间可以减少请求次数。', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<select name="cache_ttl" class="wpbridge-form-select" style="max-width: 150px;">
|
||||||
|
<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>
|
||||||
|
</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( 'HTTP 请求的超时时间(5-60 秒),网络较慢时可适当增加。', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<input type="number"
|
||||||
|
name="request_timeout"
|
||||||
|
value="<?php echo esc_attr( $settings['request_timeout'] ?? 10 ); ?>"
|
||||||
|
min="5"
|
||||||
|
max="60"
|
||||||
|
class="wpbridge-form-input"
|
||||||
|
style="max-width: 80px;">
|
||||||
|
<span style="color: var(--wpbridge-gray-500);"><?php esc_html_e( '秒', 'wpbridge' ); ?></span>
|
||||||
|
</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( '当更新源不可用时,使用过期的缓存数据作为兜底。', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<label class="wpbridge-toggle">
|
||||||
|
<input type="checkbox" name="fallback_enabled" value="1" <?php checked( $settings['fallback_enabled'] ?? true ); ?>>
|
||||||
|
<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( '在更新插件/主题前自动创建备份,支持一键回滚。', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<label class="wpbridge-toggle">
|
||||||
|
<input type="checkbox" name="backup_enabled" value="1" <?php checked( $settings['backup_enabled'] ?? true ); ?>>
|
||||||
|
<span class="wpbridge-toggle-track"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bridge Server 配置 -->
|
||||||
|
<div class="wpbridge-settings-panel" style="margin-top: 24px; border-top: 1px solid var(--wpbridge-gray-200); padding-top: 24px;">
|
||||||
|
<h3 style="margin: 0 0 16px; font-size: 14px; font-weight: 600; color: var(--wpbridge-gray-700);">
|
||||||
|
<span class="dashicons dashicons-cloud" style="margin-right: 4px;"></span>
|
||||||
|
<?php esc_html_e( 'Bridge Server', 'wpbridge' ); ?>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<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( 'wpbridge-server 服务端 URL,用于商业插件下载代理。', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<input type="url"
|
||||||
|
name="bridge_server_url"
|
||||||
|
value="<?php echo esc_attr( $settings['bridge_server_url'] ?? '' ); ?>"
|
||||||
|
placeholder="https://bridge.example.com"
|
||||||
|
class="wpbridge-form-input"
|
||||||
|
style="max-width: 300px;">
|
||||||
|
</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( '用于访问 Bridge Server 管理 API 的密钥。', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<input type="password"
|
||||||
|
name="bridge_server_api_key"
|
||||||
|
value="<?php echo esc_attr( $settings['bridge_server_api_key'] ?? '' ); ?>"
|
||||||
|
placeholder="<?php esc_attr_e( '输入 API Key', 'wpbridge' ); ?>"
|
||||||
|
class="wpbridge-form-input"
|
||||||
|
style="max-width: 300px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( ! empty( $settings['bridge_server_url'] ) ) : ?>
|
||||||
|
<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( '测试与 Bridge Server 的连接。', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="wpbridge-btn wpbridge-btn-secondary" id="wpbridge-test-bridge-server">
|
||||||
|
<span class="dashicons dashicons-update"></span>
|
||||||
|
<?php esc_html_e( '测试连接', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- 配置导入导出 -->
|
||||||
|
<div class="wpbridge-settings-panel" style="margin-top: 32px;">
|
||||||
|
<h2 class="wpbridge-section-title" style="margin-bottom: 16px;">
|
||||||
|
<span class="dashicons dashicons-database-export"></span>
|
||||||
|
<?php esc_html_e( '配置导入导出', 'wpbridge' ); ?>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<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( '将当前配置导出为 JSON 文件,用于备份或迁移到其他站点。', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--wpbridge-gray-600);">
|
||||||
|
<input type="checkbox" id="wpbridge-export-secrets">
|
||||||
|
<?php esc_html_e( '包含敏感信息', 'wpbridge' ); ?>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="wpbridge-btn wpbridge-btn-secondary" id="wpbridge-export-config">
|
||||||
|
<span class="dashicons dashicons-download"></span>
|
||||||
|
<?php esc_html_e( '导出', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
</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( '从 JSON 文件导入配置。可选择合并或覆盖现有配置。', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--wpbridge-gray-600);">
|
||||||
|
<input type="checkbox" id="wpbridge-import-merge" checked>
|
||||||
|
<?php esc_html_e( '合并配置', 'wpbridge' ); ?>
|
||||||
|
</label>
|
||||||
|
<input type="file" id="wpbridge-import-file" accept=".json" style="display: none;">
|
||||||
|
<button type="button" class="wpbridge-btn wpbridge-btn-secondary" id="wpbridge-import-config">
|
||||||
|
<span class="dashicons dashicons-upload"></span>
|
||||||
|
<?php esc_html_e( '导入', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
162
templates/admin/tabs/sources.php
Normal file
162
templates/admin/tabs/sources.php
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 更新源 Tab 内容
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.5.0
|
||||||
|
* @var array $sources 源列表
|
||||||
|
* @var array $stats 统计信息
|
||||||
|
* @var array $health_status 健康状态
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use WPBridge\UpdateSource\SourceType;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- 统计面板 -->
|
||||||
|
<div class="wpbridge-stats-panel">
|
||||||
|
<div class="wpbridge-stat-card">
|
||||||
|
<div class="wpbridge-stat-card-header">
|
||||||
|
<span class="dashicons dashicons-database"></span>
|
||||||
|
<?php esc_html_e( '总更新源', 'wpbridge' ); ?>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-stat-value"><?php echo esc_html( $stats['total'] ); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-stat-card">
|
||||||
|
<div class="wpbridge-stat-card-header">
|
||||||
|
<span class="dashicons dashicons-yes-alt"></span>
|
||||||
|
<?php esc_html_e( '已启用', 'wpbridge' ); ?>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-stat-value success"><?php echo esc_html( $stats['enabled'] ); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-stat-card">
|
||||||
|
<div class="wpbridge-stat-card-header">
|
||||||
|
<span class="dashicons dashicons-update"></span>
|
||||||
|
<?php esc_html_e( '缓存状态', 'wpbridge' ); ?>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-stat-value">
|
||||||
|
<button type="button" class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-clear-cache">
|
||||||
|
<span class="dashicons dashicons-trash"></span>
|
||||||
|
<?php esc_html_e( '清除缓存', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 更新源列表 -->
|
||||||
|
<div class="wpbridge-sources-header">
|
||||||
|
<h2 class="wpbridge-sources-title"><?php esc_html_e( '更新源列表', 'wpbridge' ); ?></h2>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="wpbridge-btn wpbridge-btn-primary">
|
||||||
|
<span class="dashicons dashicons-plus-alt2"></span>
|
||||||
|
<?php esc_html_e( '添加更新源', 'wpbridge' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( empty( $sources ) ) : ?>
|
||||||
|
<div class="wpbridge-empty">
|
||||||
|
<span class="dashicons dashicons-cloud"></span>
|
||||||
|
<h3 class="wpbridge-empty-title"><?php esc_html_e( '暂无更新源', 'wpbridge' ); ?></h3>
|
||||||
|
<p class="wpbridge-empty-desc"><?php esc_html_e( '添加自定义更新源来管理插件和主题的更新。', 'wpbridge' ); ?></p>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=add' ) ); ?>" class="wpbridge-btn wpbridge-btn-primary">
|
||||||
|
<?php esc_html_e( '添加第一个更新源', 'wpbridge' ); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<div class="wpbridge-sources-grid">
|
||||||
|
<?php foreach ( $sources as $source ) : ?>
|
||||||
|
<div class="wpbridge-source-card" data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||||
|
<div class="wpbridge-source-card-header">
|
||||||
|
<div class="wpbridge-source-card-title">
|
||||||
|
<h3 class="wpbridge-source-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; ?>
|
||||||
|
</h3>
|
||||||
|
<span class="wpbridge-source-url"><?php echo esc_html( $source->api_url ); ?></span>
|
||||||
|
</div>
|
||||||
|
<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-track"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wpbridge-source-card-body">
|
||||||
|
<div class="wpbridge-source-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-meta-item">
|
||||||
|
<span class="dashicons dashicons-admin-plugins"></span>
|
||||||
|
<?php echo esc_html( $source->item_type === 'plugin' ? __( '插件', 'wpbridge' ) : __( '主题', 'wpbridge' ) ); ?>
|
||||||
|
</span>
|
||||||
|
<?php if ( ! empty( $source->slug ) ) : ?>
|
||||||
|
<span class="wpbridge-source-meta-item">
|
||||||
|
<span class="dashicons dashicons-tag"></span>
|
||||||
|
<?php echo esc_html( $source->slug ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span class="wpbridge-source-meta-item">
|
||||||
|
<span class="dashicons dashicons-sort"></span>
|
||||||
|
<?php
|
||||||
|
/* translators: %d: priority number */
|
||||||
|
printf( esc_html__( '优先级 %d', 'wpbridge' ), $source->priority );
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ( isset( $health_status[ $source->id ] ) && is_array( $health_status[ $source->id ] ) ) : ?>
|
||||||
|
<span class="wpbridge-badge wpbridge-badge-status <?php echo esc_attr( $health_status[ $source->id ]['status'] ?? '' ); ?>">
|
||||||
|
<?php
|
||||||
|
$status_labels = [
|
||||||
|
'healthy' => __( '正常', 'wpbridge' ),
|
||||||
|
'degraded' => __( '降级', 'wpbridge' ),
|
||||||
|
'failed' => __( '失败', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
$current_status = $health_status[ $source->id ]['status'] ?? 'unknown';
|
||||||
|
echo esc_html( $status_labels[ $current_status ] ?? $current_status );
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wpbridge-source-card-footer">
|
||||||
|
<div class="wpbridge-source-actions">
|
||||||
|
<button type="button"
|
||||||
|
class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm wpbridge-test-source"
|
||||||
|
data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||||
|
<span class="dashicons dashicons-admin-site-alt3"></span>
|
||||||
|
<?php esc_html_e( '测试', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
<a href="<?php echo esc_url( admin_url( 'admin.php?page=wpbridge&action=edit&source=' . $source->id ) ); ?>"
|
||||||
|
class="wpbridge-btn wpbridge-btn-secondary wpbridge-btn-sm">
|
||||||
|
<span class="dashicons dashicons-edit"></span>
|
||||||
|
<?php esc_html_e( '编辑', 'wpbridge' ); ?>
|
||||||
|
</a>
|
||||||
|
<?php if ( ! $source->is_preset ) : ?>
|
||||||
|
<button type="button"
|
||||||
|
class="wpbridge-btn wpbridge-btn-danger wpbridge-btn-sm wpbridge-delete-source"
|
||||||
|
data-source-id="<?php echo esc_attr( $source->id ); ?>">
|
||||||
|
<span class="dashicons dashicons-trash"></span>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- 删除确认表单 -->
|
||||||
|
<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>
|
||||||
345
templates/admin/tabs/vendors.php
Normal file
345
templates/admin/tabs/vendors.php
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 供应商管理 Tab
|
||||||
|
*
|
||||||
|
* @package WPBridge
|
||||||
|
* @since 0.9.8
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 防止直接访问
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
use WPBridge\Admin\VendorAdmin;
|
||||||
|
use WPBridge\Core\Settings;
|
||||||
|
|
||||||
|
$settings_obj = new Settings();
|
||||||
|
$vendor_admin = new VendorAdmin( $settings_obj );
|
||||||
|
$vendor_data = $vendor_admin->get_vendor_data();
|
||||||
|
|
||||||
|
$vendors = $vendor_data['vendors'];
|
||||||
|
$custom = $vendor_data['custom'];
|
||||||
|
$all_plugins = $vendor_data['all_plugins'];
|
||||||
|
$stats = $vendor_data['stats'];
|
||||||
|
$vendor_types = $vendor_data['vendor_types'];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wpbridge-vendors-section">
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="wpbridge-stats-row">
|
||||||
|
<div class="wpbridge-stat-card">
|
||||||
|
<span class="wpbridge-stat-number"><?php echo esc_html( count( $vendors ) ); ?></span>
|
||||||
|
<span class="wpbridge-stat-label"><?php esc_html_e( '供应商', 'wpbridge' ); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-stat-card">
|
||||||
|
<span class="wpbridge-stat-number"><?php echo esc_html( count( $all_plugins ) ); ?></span>
|
||||||
|
<span class="wpbridge-stat-label"><?php esc_html_e( '可用插件', 'wpbridge' ); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-stat-card">
|
||||||
|
<span class="wpbridge-stat-number"><?php echo esc_html( $stats['bridged_count'] ?? 0 ); ?></span>
|
||||||
|
<span class="wpbridge-stat-label"><?php esc_html_e( '已桥接', 'wpbridge' ); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-stat-card">
|
||||||
|
<span class="wpbridge-stat-number"><?php echo esc_html( count( $custom ) ); ?></span>
|
||||||
|
<span class="wpbridge-stat-label"><?php esc_html_e( '自定义', 'wpbridge' ); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 供应商列表 -->
|
||||||
|
<div class="wpbridge-section">
|
||||||
|
<div class="wpbridge-section-header">
|
||||||
|
<h3><?php esc_html_e( '供应商渠道', 'wpbridge' ); ?></h3>
|
||||||
|
<button type="button" class="button button-primary" id="wpbridge-add-vendor-btn">
|
||||||
|
<span class="dashicons dashicons-plus-alt2"></span>
|
||||||
|
<?php esc_html_e( '添加供应商', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="wpbridge-section-desc">
|
||||||
|
<?php esc_html_e( '接入第三方 GPL 插件分发商,获取更多商业插件的更新支持。', 'wpbridge' ); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if ( empty( $vendors ) ) : ?>
|
||||||
|
<div class="wpbridge-empty-state">
|
||||||
|
<span class="dashicons dashicons-store"></span>
|
||||||
|
<p><?php esc_html_e( '暂无供应商,点击上方按钮添加', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<table class="wp-list-table widefat fixed striped wpbridge-vendors-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-plugins"><?php esc_html_e( '插件数', 'wpbridge' ); ?></th>
|
||||||
|
<th class="column-actions"><?php esc_html_e( '操作', 'wpbridge' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( $vendors as $vendor_id => $vendor ) : ?>
|
||||||
|
<tr data-vendor-id="<?php echo esc_attr( $vendor_id ); ?>">
|
||||||
|
<td class="column-status">
|
||||||
|
<label class="wpbridge-toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="wpbridge-vendor-toggle"
|
||||||
|
data-vendor-id="<?php echo esc_attr( $vendor_id ); ?>"
|
||||||
|
<?php checked( ! empty( $vendor['enabled'] ) ); ?>>
|
||||||
|
<span class="wpbridge-toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td class="column-name">
|
||||||
|
<strong><?php echo esc_html( $vendor['name'] ); ?></strong>
|
||||||
|
<div class="row-actions">
|
||||||
|
<span class="wpbridge-vendor-url">
|
||||||
|
<?php echo esc_html( $vendor['api_url'] ?? '' ); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="column-type">
|
||||||
|
<?php echo esc_html( $vendor_types[ $vendor['type'] ] ?? $vendor['type'] ); ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-plugins">
|
||||||
|
<span class="wpbridge-plugin-count">-</span>
|
||||||
|
</td>
|
||||||
|
<td class="column-actions">
|
||||||
|
<button type="button" class="button wpbridge-test-vendor"
|
||||||
|
data-vendor-id="<?php echo esc_attr( $vendor_id ); ?>">
|
||||||
|
<?php esc_html_e( '测试', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button wpbridge-sync-vendor"
|
||||||
|
data-vendor-id="<?php echo esc_attr( $vendor_id ); ?>">
|
||||||
|
<?php esc_html_e( '同步', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button button-link-delete wpbridge-remove-vendor"
|
||||||
|
data-vendor-id="<?php echo esc_attr( $vendor_id ); ?>">
|
||||||
|
<?php esc_html_e( '删除', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义插件 -->
|
||||||
|
<div class="wpbridge-section">
|
||||||
|
<div class="wpbridge-section-header">
|
||||||
|
<h3><?php esc_html_e( '自定义插件', 'wpbridge' ); ?></h3>
|
||||||
|
<button type="button" class="button" id="wpbridge-add-custom-btn">
|
||||||
|
<span class="dashicons dashicons-plus-alt2"></span>
|
||||||
|
<?php esc_html_e( '添加插件', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="wpbridge-section-desc">
|
||||||
|
<?php esc_html_e( '手动添加不在官方列表或供应商渠道中的插件。', 'wpbridge' ); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if ( empty( $custom ) ) : ?>
|
||||||
|
<div class="wpbridge-empty-state">
|
||||||
|
<span class="dashicons dashicons-admin-plugins"></span>
|
||||||
|
<p><?php esc_html_e( '暂无自定义插件', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<table class="wp-list-table widefat fixed striped wpbridge-custom-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="column-slug"><?php esc_html_e( 'Slug', 'wpbridge' ); ?></th>
|
||||||
|
<th class="column-name"><?php esc_html_e( '名称', 'wpbridge' ); ?></th>
|
||||||
|
<th class="column-url"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></th>
|
||||||
|
<th class="column-actions"><?php esc_html_e( '操作', 'wpbridge' ); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ( $custom as $slug => $info ) : ?>
|
||||||
|
<tr data-plugin-slug="<?php echo esc_attr( $slug ); ?>">
|
||||||
|
<td class="column-slug">
|
||||||
|
<code><?php echo esc_html( $slug ); ?></code>
|
||||||
|
</td>
|
||||||
|
<td class="column-name">
|
||||||
|
<?php echo esc_html( $info['name'] ?? $slug ); ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-url">
|
||||||
|
<?php echo esc_html( $info['update_url'] ?? '-' ); ?>
|
||||||
|
</td>
|
||||||
|
<td class="column-actions">
|
||||||
|
<button type="button" class="button button-link-delete wpbridge-remove-custom"
|
||||||
|
data-plugin-slug="<?php echo esc_attr( $slug ); ?>">
|
||||||
|
<?php esc_html_e( '删除', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 可用插件列表 -->
|
||||||
|
<div class="wpbridge-section">
|
||||||
|
<div class="wpbridge-section-header">
|
||||||
|
<h3><?php esc_html_e( '可用插件', 'wpbridge' ); ?></h3>
|
||||||
|
<button type="button" class="button" id="wpbridge-sync-all-btn">
|
||||||
|
<span class="dashicons dashicons-update"></span>
|
||||||
|
<?php esc_html_e( '同步全部', 'wpbridge' ); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="wpbridge-section-desc">
|
||||||
|
<?php esc_html_e( '来自官方列表、供应商渠道和自定义的所有可桥接插件。', 'wpbridge' ); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if ( empty( $all_plugins ) ) : ?>
|
||||||
|
<div class="wpbridge-empty-state">
|
||||||
|
<span class="dashicons dashicons-admin-plugins"></span>
|
||||||
|
<p><?php esc_html_e( '暂无可用插件,请添加供应商或同步官方列表', 'wpbridge' ); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<div class="wpbridge-plugins-grid">
|
||||||
|
<?php foreach ( $all_plugins as $slug => $info ) : ?>
|
||||||
|
<div class="wpbridge-plugin-card" data-slug="<?php echo esc_attr( $slug ); ?>">
|
||||||
|
<div class="wpbridge-plugin-header">
|
||||||
|
<span class="wpbridge-plugin-name">
|
||||||
|
<?php echo esc_html( $info['name'] ?? $slug ); ?>
|
||||||
|
</span>
|
||||||
|
<span class="wpbridge-plugin-source wpbridge-source-<?php echo esc_attr( $info['source'] ?? 'unknown' ); ?>">
|
||||||
|
<?php
|
||||||
|
$source_labels = [
|
||||||
|
'official' => __( '官方', 'wpbridge' ),
|
||||||
|
'vendor' => __( '供应商', 'wpbridge' ),
|
||||||
|
'custom' => __( '自定义', 'wpbridge' ),
|
||||||
|
];
|
||||||
|
echo esc_html( $source_labels[ $info['source'] ] ?? $info['source'] );
|
||||||
|
?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-plugin-meta">
|
||||||
|
<code><?php echo esc_html( $slug ); ?></code>
|
||||||
|
<?php if ( ! empty( $info['vendor'] ) ) : ?>
|
||||||
|
<span class="wpbridge-plugin-vendor">
|
||||||
|
<?php echo esc_html( $info['vendor'] ); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加供应商弹窗 -->
|
||||||
|
<div id="wpbridge-vendor-modal" class="wpbridge-modal" style="display:none;">
|
||||||
|
<div class="wpbridge-modal-content">
|
||||||
|
<div class="wpbridge-modal-header">
|
||||||
|
<h2><?php esc_html_e( '添加供应商', 'wpbridge' ); ?></h2>
|
||||||
|
<button type="button" class="wpbridge-modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-modal-body">
|
||||||
|
<form id="wpbridge-vendor-form">
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th><label for="vendor_id"><?php esc_html_e( '供应商 ID', 'wpbridge' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="vendor_id" name="vendor_id" class="regular-text" required
|
||||||
|
pattern="[a-z0-9_-]+" placeholder="my-vendor">
|
||||||
|
<p class="description"><?php esc_html_e( '唯一标识符,只能包含小写字母、数字、下划线和连字符', 'wpbridge' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="vendor_name"><?php esc_html_e( '名称', 'wpbridge' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="vendor_name" name="name" class="regular-text" required
|
||||||
|
placeholder="<?php esc_attr_e( '我的供应商', 'wpbridge' ); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="vendor_type"><?php esc_html_e( '类型', 'wpbridge' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<select id="vendor_type" name="type">
|
||||||
|
<?php foreach ( $vendor_types as $type => $label ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $type ); ?>">
|
||||||
|
<?php echo esc_html( $label ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="vendor_api_url"><?php esc_html_e( 'API 地址', 'wpbridge' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input type="url" id="vendor_api_url" name="api_url" class="regular-text" required
|
||||||
|
placeholder="https://example.com">
|
||||||
|
<p class="description"><?php esc_html_e( 'WooCommerce 商店的根地址', 'wpbridge' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="vendor_consumer_key"><?php esc_html_e( 'Consumer Key', 'wpbridge' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="vendor_consumer_key" name="consumer_key" class="regular-text"
|
||||||
|
placeholder="ck_xxxxxxxx">
|
||||||
|
<p class="description"><?php esc_html_e( 'WooCommerce REST API Consumer Key(可选)', 'wpbridge' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="vendor_consumer_secret"><?php esc_html_e( 'Consumer Secret', 'wpbridge' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input type="password" id="vendor_consumer_secret" name="consumer_secret" class="regular-text"
|
||||||
|
placeholder="cs_xxxxxxxx">
|
||||||
|
<p class="description"><?php esc_html_e( 'WooCommerce REST API Consumer Secret(可选)', 'wpbridge' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-modal-footer">
|
||||||
|
<button type="button" class="button wpbridge-modal-cancel"><?php esc_html_e( '取消', 'wpbridge' ); ?></button>
|
||||||
|
<button type="button" class="button button-primary" id="wpbridge-save-vendor"><?php esc_html_e( '保存', 'wpbridge' ); ?></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加自定义插件弹窗 -->
|
||||||
|
<div id="wpbridge-custom-modal" class="wpbridge-modal" style="display:none;">
|
||||||
|
<div class="wpbridge-modal-content">
|
||||||
|
<div class="wpbridge-modal-header">
|
||||||
|
<h2><?php esc_html_e( '添加自定义插件', 'wpbridge' ); ?></h2>
|
||||||
|
<button type="button" class="wpbridge-modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-modal-body">
|
||||||
|
<form id="wpbridge-custom-form">
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th><label for="custom_slug"><?php esc_html_e( '插件 Slug', 'wpbridge' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="custom_slug" name="plugin_slug" class="regular-text" required
|
||||||
|
pattern="[a-z0-9_-]+" placeholder="my-plugin">
|
||||||
|
<p class="description"><?php esc_html_e( '插件目录名称', 'wpbridge' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="custom_name"><?php esc_html_e( '名称', 'wpbridge' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="custom_name" name="name" class="regular-text"
|
||||||
|
placeholder="<?php esc_attr_e( '我的插件', 'wpbridge' ); ?>">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th><label for="custom_url"><?php esc_html_e( '更新地址', 'wpbridge' ); ?></label></th>
|
||||||
|
<td>
|
||||||
|
<input type="url" id="custom_url" name="update_url" class="regular-text"
|
||||||
|
placeholder="https://example.com/update.json">
|
||||||
|
<p class="description"><?php esc_html_e( '插件更新检查地址(可选)', 'wpbridge' ); ?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="wpbridge-modal-footer">
|
||||||
|
<button type="button" class="button wpbridge-modal-cancel"><?php esc_html_e( '取消', 'wpbridge' ); ?></button>
|
||||||
|
<button type="button" class="button button-primary" id="wpbridge-save-custom"><?php esc_html_e( '保存', 'wpbridge' ); ?></button>
|
||||||
|
</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