Compare commits

...

No commits in common. "dd8fa5f82e2c966498fc08f3c2ed4fc8c2eff9c8" and "bd3a4379bd79c45308cccac13260f3f6329c910a" have entirely different histories.

22 changed files with 4878 additions and 6286 deletions

View file

@ -1,119 +0,0 @@
name: Auto Label

on:
pull_request:
types: [opened, synchronize]

jobs:
auto-label:
if: github.repository != 'WenPai-org/ci-workflows'
runs-on: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: 自动打标签
env:
FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
API_BASE="${GITHUB_SERVER_URL:-https://feicode.com}/api/v1"
OWNER="${GITHUB_REPOSITORY%/*}"
REPO="${GITHUB_REPOSITORY#*/}"
LABELS=""

# 获取 PR 变更文件
FILES=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/pulls/$PR_NUMBER/files" | \
python3 -c "import json,sys; [print(f['filename']) for f in json.load(sys.stdin)]" 2>/dev/null)

# 根据文件类型打标签
echo "$FILES" | grep -qE '\.php$' && LABELS="$LABELS,php"
echo "$FILES" | grep -qE '\.(js|ts|tsx|jsx)$' && LABELS="$LABELS,frontend"
echo "$FILES" | grep -qE '\.(css|scss|less)$' && LABELS="$LABELS,style"
echo "$FILES" | grep -qE '\.go$' && LABELS="$LABELS,go"
echo "$FILES" | grep -qE '\.(yml|yaml)$' && LABELS="$LABELS,ci/cd"
echo "$FILES" | grep -qE '(composer\.|package\.json|go\.mod)' && LABELS="$LABELS,dependencies"
echo "$FILES" | grep -qE '\.(md|txt|rst)$' && LABELS="$LABELS,documentation"
echo "$FILES" | grep -qE '(Dockerfile|docker-compose)' && LABELS="$LABELS,docker"

# 根据 PR 大小打标签
ADDITIONS=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/pulls/$PR_NUMBER" | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('additions',0)+d.get('deletions',0))" 2>/dev/null)

if [ "$ADDITIONS" -lt 10 ] 2>/dev/null; then
LABELS="$LABELS,size/S"
elif [ "$ADDITIONS" -lt 100 ] 2>/dev/null; then
LABELS="$LABELS,size/M"
elif [ "$ADDITIONS" -lt 500 ] 2>/dev/null; then
LABELS="$LABELS,size/L"
else
LABELS="$LABELS,size/XL"
fi

# 去掉开头逗号
LABELS="${LABELS#,}"

if [ -z "$LABELS" ]; then
echo "无需添加标签"
exit 0
fi

echo "添加标签: $LABELS"

# 确保标签存在,不存在则创建
IFS=',' read -ra LABEL_ARRAY <<< "$LABELS"
LABEL_IDS="["
for LABEL in "${LABEL_ARRAY[@]}"; do
# 查找标签
LABEL_ID=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/labels?limit=50" | \
python3 -c "
import json,sys
labels=json.load(sys.stdin)
for l in labels:
if l['name']=='$LABEL':
print(l['id'])
break
" 2>/dev/null)

# 标签不存在则创建
if [ -z "$LABEL_ID" ]; then
# 根据标签类型选颜色
case "$LABEL" in
php) COLOR="#4F5D95" ;;
frontend) COLOR="#f1e05a" ;;
go) COLOR="#00ADD8" ;;
ci/cd) COLOR="#0075ca" ;;
dependencies) COLOR="#0366d6" ;;
documentation) COLOR="#0075ca" ;;
docker) COLOR="#0db7ed" ;;
style) COLOR="#e34c26" ;;
size/S) COLOR="#009900" ;;
size/M) COLOR="#FFCC00" ;;
size/L) COLOR="#FF6600" ;;
size/XL) COLOR="#CC0000" ;;
*) COLOR="#ededed" ;;
esac
LABEL_ID=$(curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/labels" \
-H "Content-Type: application/json" \
-d "{\"name\":\"$LABEL\",\"color\":\"$COLOR\"}" | \
python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
fi

[ -n "$LABEL_ID" ] && LABEL_IDS="$LABEL_IDS$LABEL_ID,"
done
LABEL_IDS="${LABEL_IDS%,}]"

# 添加标签到 PR
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues/$PR_NUMBER/labels" \
-H "Content-Type: application/json" \
-d "{\"labels\":$LABEL_IDS}" > /dev/null

echo "标签添加完成"

View file

@ -1,31 +0,0 @@
name: gitleaks 密钥泄露扫描

on:
push:
branches: ['*']
pull_request:
branches: ['*']

jobs:
gitleaks:
runs-on: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Run gitleaks
run: |
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
gitleaks detect --source=. --log-opts="$GITHUB_SHA~1..$GITHUB_SHA" --verbose --exit-code 1 || {
echo "::error::gitleaks 发现了潜在的密钥泄露!请检查上方输出并移除敏感信息。"
exit 1
}
else
gitleaks detect --source=. --verbose --exit-code 1 || {
echo "::error::gitleaks 发现了潜在的密钥泄露!请检查上方输出并移除敏感信息。"
exit 1
}
fi
echo "gitleaks 扫描通过,未发现密钥泄露。"

View file

@ -127,7 +127,6 @@ jobs:
--exclude="*.md" \
--exclude="LICENSE" \
--exclude="Makefile" \
--exclude="lib" \
./ "$BUILD_DIR/"

(
@ -361,39 +360,3 @@ jobs:
fi

echo "Update API verification passed"

- name: Notify AI CI assistant on failure (optional)
if: ${{ failure() && secrets.AI_WEBHOOK_ENDPOINT != '' }}
continue-on-error: true
shell: bash
env:
AI_WEBHOOK_ENDPOINT: ${{ secrets.AI_WEBHOOK_ENDPOINT }}
AI_WEBHOOK_TOKEN: ${{ secrets.AI_WEBHOOK_TOKEN }}
run: |
set -euo pipefail

OWNER="${GITHUB_REPOSITORY%%/*}"
REPO="${GITHUB_REPOSITORY##*/}"
BRANCH="${GITHUB_REF#refs/heads/}"
RUN_URL="${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
FAILED_STEPS_JSON="$(printf '%s' '${{ toJson(steps) }}' | jq -c '[to_entries[] | select(.value.outcome=="failure" or .value.conclusion=="failure") | .key]')"

PAYLOAD="$(jq -cn \
--arg event "ci_failure_report" \
--arg owner "$OWNER" \
--arg repo "$REPO" \
--arg sha "${GITHUB_SHA}" \
--arg branch "$BRANCH" \
--arg workflow "${GITHUB_WORKFLOW}" \
--arg run_url "$RUN_URL" \
--arg log_excerpt "release workflow failed; inspect run_url for full logs" \
--argjson failed_steps "$FAILED_STEPS_JSON" \
'{event:$event, owner:$owner, repo:$repo, sha:$sha, branch:$branch, workflow:$workflow, run_url:$run_url, failed_steps:$failed_steps, log_excerpt:$log_excerpt}')"

CURL_HEADERS=(-H "Content-Type: application/json")
if [[ -n "${AI_WEBHOOK_TOKEN:-}" ]]; then
CURL_HEADERS+=( -H "Authorization: Bearer ${AI_WEBHOOK_TOKEN}" )
fi

curl -sS -X POST "${CURL_HEADERS[@]}" --data "$PAYLOAD" "$AI_WEBHOOK_ENDPOINT" > /dev/null
echo "AI CI assistant notified"

View file

@ -1,128 +0,0 @@
name: 安全扫描

on:
push:
branches: ['main', 'master']
paths:
- 'Dockerfile*'
- 'docker-compose*.yml'
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
- 'yarn.lock'
- 'pnpm-lock.yaml'
- 'go.mod'
- 'go.sum'
pull_request:
branches: ['main', 'master']
schedule:
- cron: '0 3 * * 1' # 每周一凌晨 3 点

jobs:
security-scan:
runs-on: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Hadolint Dockerfile 检查
run: |
# 检查是否存在 Dockerfile
DOCKERFILES=$(find . -name 'Dockerfile*' -not -path './.git/*' 2>/dev/null)
if [ -z "$DOCKERFILES" ]; then
echo "未找到 Dockerfile跳过"
exit 0
fi
# 如果未安装 hadolint 则跳过
if ! command -v hadolint &> /dev/null; then
echo "hadolint 未安装,跳过 Dockerfile 检查"
exit 0
fi
FAILED=0
for df in $DOCKERFILES; do
echo "--- 检查 $df ---"
hadolint "$df" || FAILED=1
done
if [ $FAILED -eq 1 ]; then
echo "::warning::Dockerfile 存在规范问题,请检查上方输出"
fi

- name: PHP Composer 安全审计
run: |
if [ ! -f composer.lock ]; then
echo "未找到 composer.lock跳过"
exit 0
fi
composer audit --format=table || {
echo "::error::Composer 依赖存在已知安全漏洞"
exit 1
}
echo "Composer 安全审计通过"

- name: npm 安全审计
run: |
if [ ! -f package-lock.json ] && [ ! -f yarn.lock ] && [ ! -f pnpm-lock.yaml ]; then
echo "未找到 JS 锁文件,跳过"
exit 0
fi
# npm audit 只报告 high 和 critical
if [ -f package-lock.json ]; then
npm audit --audit-level=high || {
echo "::error::npm 依赖存在高危安全漏洞"
exit 1
}
elif [ -f pnpm-lock.yaml ]; then
pnpm audit --audit-level=high || {
echo "::error::pnpm 依赖存在高危安全漏洞"
exit 1
}
elif [ -f yarn.lock ]; then
yarn npm audit --severity high || {
echo "::error::yarn 依赖存在高危安全漏洞"
exit 1
}
fi
echo "JS 依赖安全审计通过"

- name: Go 依赖漏洞检查
run: |
if [ ! -f go.mod ]; then
echo "未找到 go.mod跳过"
exit 0
fi
# govulncheck 检查已知漏洞
if ! command -v govulncheck &> /dev/null; then
go install golang.org/x/vuln/cmd/govulncheck@latest
export PATH="$(go env GOPATH)/bin:$PATH"
fi
govulncheck ./... || {
echo "::error::Go 依赖存在已知安全漏洞"
exit 1
}
echo "Go 依赖安全检查通过"

- name: Trivy 容器镜像扫描
run: |
DOCKERFILES=$(find . -name 'Dockerfile' -not -path './.git/*' 2>/dev/null)
if [ -z "$DOCKERFILES" ]; then
echo "未找到 Dockerfile跳过镜像扫描"
exit 0
fi
if ! command -v trivy &> /dev/null; then
echo "trivy 未安装,跳过容器镜像扫描"
exit 0
fi
# 构建并扫描镜像
IMAGE_NAME="ci-security-scan:$$"
podman build -t "$IMAGE_NAME" -f Dockerfile . || {
echo "::warning::容器构建失败,跳过镜像扫描"
exit 0
}
trivy image --severity HIGH,CRITICAL --exit-code 1 "$IMAGE_NAME" || {
echo "::error::容器镜像存在高危漏洞"
podman rmi "$IMAGE_NAME" 2>/dev/null
exit 1
}
podman rmi "$IMAGE_NAME" 2>/dev/null
echo "容器镜像安全扫描通过"

View file

@ -1,119 +0,0 @@
name: Stale Issue/PR 清理

on:
schedule:
- cron: '0 5 * * 1' # 每周一凌晨 5 点
workflow_dispatch:

jobs:
stale-cleanup:
if: github.repository != 'WenPai-org/ci-workflows'
runs-on: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4

- name: 清理过期 Issue 和 PR
env:
FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
API_BASE="${GITHUB_SERVER_URL:-https://feicode.com}/api/v1"
OWNER="${GITHUB_REPOSITORY%/*}"
REPO="${GITHUB_REPOSITORY#*/}"
NOW=$(date +%s)

# 受保护的标签,带这些标签的不处理
PROTECTED_LABELS="pinned|help-wanted|bug|security|wontfix"

# Issue: 60 天无活动标记 stale再过 14 天关闭
STALE_DAYS=60
CLOSE_DAYS=74
# PR: 30 天无活动标记 stale再过 14 天关闭
PR_STALE_DAYS=30
PR_CLOSE_DAYS=44

process_items() {
local TYPE=$1 # issues 或 pulls
local STALE=$2
local CLOSE=$3

PAGE=1
while true; do
if [ "$TYPE" = "issues" ]; then
ITEMS=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues?state=open&type=issues&page=$PAGE&limit=50")
else
ITEMS=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues?state=open&type=pulls&page=$PAGE&limit=50")
fi

COUNT=$(echo "$ITEMS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null)
[ "$COUNT" = "0" ] || [ -z "$COUNT" ] && break

echo "$ITEMS" | python3 -c "
import json, sys, time

items = json.load(sys.stdin)
now = $NOW
stale_seconds = $STALE * 86400
close_seconds = $CLOSE * 86400
protected = set('$PROTECTED_LABELS'.split('|'))

for item in items:
number = item['number']
title = item['title']
labels = [l['name'] for l in item.get('labels', [])]

# 跳过受保护标签
if protected & set(labels):
continue

updated = item['updated_at']
# 解析 ISO 时间
from datetime import datetime, timezone
dt = datetime.fromisoformat(updated.replace('Z', '+00:00'))
updated_ts = int(dt.timestamp())
age = now - updated_ts

if age > close_seconds:
print(f'CLOSE|{number}|{title}')
elif age > stale_seconds and 'stale' not in labels:
print(f'STALE|{number}|{title}')
" | while IFS='|' read -r ACTION NUMBER TITLE; do
if [ "$ACTION" = "STALE" ]; then
echo "标记 #$NUMBER 为 stale: $TITLE"
# 添加 stale 标签
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER/labels" \
-H "Content-Type: application/json" \
-d '{"labels":["stale"]}' > /dev/null
# 添加评论
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER/comments" \
-H "Content-Type: application/json" \
-d "{\"body\":\"此 ${TYPE%s} 已超过 $STALE 天无活动,已标记为 stale。如果 14 天内无新活动将自动关闭。\"}" > /dev/null
elif [ "$ACTION" = "CLOSE" ]; then
echo "关闭 #$NUMBER: $TITLE"
curl -s -X PATCH -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' > /dev/null
curl -s -X POST -H "Authorization: token $FORGEJO_TOKEN" \
"$API_BASE/repos/$OWNER/$REPO/issues/$NUMBER/comments" \
-H "Content-Type: application/json" \
-d "{\"body\":\"此 ${TYPE%s} 因长期无活动已自动关闭。如需继续讨论请重新打开。\"}" > /dev/null
fi
done

PAGE=$((PAGE + 1))
done
}

echo "=== 处理 Issues (${STALE_DAYS}天stale / ${CLOSE_DAYS}天关闭) ==="
process_items "issues" $STALE_DAYS $CLOSE_DAYS

echo "=== 处理 PRs (${PR_STALE_DAYS}天stale / ${PR_CLOSE_DAYS}天关闭) ==="
process_items "pulls" $PR_STALE_DAYS $PR_CLOSE_DAYS

echo "清理完成"

View file

@ -1,34 +0,0 @@
name: Trivy 依赖漏洞扫描

on:
push:
branches: ['main', 'master']
paths:
- 'composer.lock'
- 'package-lock.json'
- 'yarn.lock'
- 'pnpm-lock.yaml'
pull_request:
branches: ['main', 'master']
# 每周一早上 8 点定时扫描
schedule:
- cron: '0 0 * * 1'

jobs:
trivy-scan:
runs-on: docker
container:
image: aquasec/trivy:latest
steps:
- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4

- name: Run Trivy filesystem scan
run: |
trivy filesystem . --scanners vuln --severity HIGH,CRITICAL --exit-code 1 --format table --ignorefile .trivyignore 2>&1 || {
echo
echo ::warning::Trivy 发现了高危/严重漏洞,请检查上方输出。
echo 如需忽略特定 CVE请在仓库根目录创建 .trivyignore 文件。
exit 1
}
echo ✅ Trivy 扫描通过,未发现高危漏洞。

View file

@ -1,41 +0,0 @@
name: WordPress 插件 CI

on:
push:
branches: ['main', 'master']
pull_request:
branches: ['main', 'master']

jobs:
ci:
if: github.repository != 'WenPai-org/ci-workflows'
runs-on: linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v4

- name: PHP Parallel Lint
run: |
parallel-lint --exclude vendor --exclude node_modules .

- name: PHPCS 代码规范检查
run: |
# 如果仓库有自定义 phpcs 配置则使用,否则用默认 WordPress 标准
if [ -f phpcs.xml ] || [ -f phpcs.xml.dist ] || [ -f .phpcs.xml ] || [ -f .phpcs.xml.dist ]; then
phpcs .
else
phpcs --standard=WordPress-Extra \
--extensions=php \
--ignore=vendor/*,node_modules/*,tests/*,lib/* \
--report=full \
-s .
fi

- name: Gitleaks 密钥泄露扫描
run: |
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
gitleaks detect --source=. --log-opts="$GITHUB_SHA~1..$GITHUB_SHA" --verbose --exit-code 1
else
gitleaks detect --source=. --verbose --exit-code 1
fi
echo "gitleaks 扫描通过"

View file

@ -1,99 +0,0 @@
name: WordPress 插件自动发布

on:
push:
tags:
- 'v*'
- '[0-9]+.[0-9]+*'

jobs:
release:
runs-on: docker
container:
image: php:8.2-cli-alpine
steps:
- name: Install system dependencies
run: |
apk add --no-cache git curl zip unzip rsync npm composer python3

- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4

- name: Install WP-CLI
run: |
curl -sO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp

- name: Install dependencies
run: |
if [ -f composer.json ]; then
composer install --no-dev --optimize-autoloader --no-interaction
fi
if [ -f package.json ]; then
npm ci --production 2>/dev/null || true
fi

- name: Run PHPCS (if configured)
run: |
if [ -f phpcs.xml ] || [ -f phpcs.xml.dist ] || [ -f .phpcs.xml ] || [ -f .phpcs.xml.dist ]; then
composer lint 2>/dev/null || vendor/bin/phpcs . || {
echo "::error::PHPCS 检查失败,请修复代码规范问题后重新打 tag。"
exit 1
}
else
echo "未找到 PHPCS 配置,跳过代码规范检查。"
fi

- name: Generate i18n files
run: |
SLUG=$(basename $GITHUB_REPOSITORY)
if [ -d languages ] || [ -d lang ]; then
LANG_DIR=$([ -d languages ] && echo languages || echo lang)
wp i18n make-pot . "$LANG_DIR/$SLUG.pot" --allow-root 2>/dev/null || true
wp i18n make-json "$LANG_DIR/" --no-purge --allow-root 2>/dev/null || true
fi

- name: Build release ZIP
run: |
PLUGIN_SLUG=$(basename $GITHUB_REPOSITORY)
TAG_NAME=${GITHUB_REF#refs/tags/}
mkdir -p /tmp/release
rsync -a --exclude='.git' --exclude='.forgejo' --exclude='.github' \
--exclude='node_modules' --exclude='.phpcs*' --exclude='phpstan*' \
--exclude='tests' --exclude='.editorconfig' --exclude='.gitignore' \
--exclude='phpunit*' --exclude='Gruntfile*' --exclude='webpack*' \
--exclude='package.json' --exclude='package-lock.json' \
--exclude='composer.lock' --exclude='.env*' \
. /tmp/release/$PLUGIN_SLUG/
cd /tmp/release
zip -r /tmp/$PLUGIN_SLUG-$TAG_NAME.zip $PLUGIN_SLUG/
echo "ZIP_PATH=/tmp/$PLUGIN_SLUG-$TAG_NAME.zip" >> $GITHUB_ENV
echo "PLUGIN_SLUG=$PLUGIN_SLUG" >> $GITHUB_ENV
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV

- name: Create Forgejo Release
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
RELEASE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases" \
-d "{
\"tag_name\": \"$TAG_NAME\",
\"name\": \"$PLUGIN_SLUG $TAG_NAME\",
\"body\": \"自动发布 $TAG_NAME\",
\"draft\": false,
\"prerelease\": false
}")
RELEASE_ID=$(echo $RELEASE_RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
if [ -z "$RELEASE_ID" ]; then
echo "::warning::Release 创建失败或已存在。"
exit 0
fi
curl -s -X POST \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$ZIP_PATH" \
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=$PLUGIN_SLUG-$TAG_NAME.zip"
echo "Release $TAG_NAME 发布成功!"

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
vendor/
node_modules/
.env
*.log
.DS_Store

View file

@ -91,13 +91,13 @@ class WenPai_Updater {
// Update URI: https://updates.wenpai.net 触发此 filter
add_filter(
'update_plugins_updates.wenpai.net',
array( $this, 'check_update' ),
[ $this, 'check_update' ],
10,
4
);

// 插件详情弹窗
add_filter( 'plugins_api', array( $this, 'plugin_info' ), 20, 3 );
add_filter( 'plugins_api', [ $this, 'plugin_info' ], 20, 3 );
}

/**
@ -117,16 +117,13 @@ class WenPai_Updater {
return $update;
}

$response = $this->api_request(
'update-check',
array(
'plugins' => array(
$this->plugin_file => array(
$response = $this->api_request( 'update-check', [
'plugins' => [
$this->plugin_file => [
'Version' => $this->version,
),
),
)
);
],
],
] );

if ( is_wp_error( $response ) || empty( $response['plugins'][ $this->plugin_file ] ) ) {
return $update;
@ -134,7 +131,7 @@ class WenPai_Updater {

$data = $response['plugins'][ $this->plugin_file ];

return (object) array(
return (object) [
'id' => $data['id'] ?? '',
'slug' => $data['slug'] ?? $this->slug,
'plugin' => $this->plugin_file,
@ -142,12 +139,12 @@ class WenPai_Updater {
'new_version' => $data['version'] ?? '',
'url' => $data['url'] ?? '',
'package' => $data['package'] ?? '',
'icons' => $data['icons'] ?? array(),
'banners' => $data['banners'] ?? array(),
'icons' => $data['icons'] ?? [],
'banners' => $data['banners'] ?? [],
'requires' => $data['requires'] ?? '',
'tested' => $data['tested'] ?? '',
'requires_php' => $data['requires_php'] ?? '',
);
];
}

/**
@ -186,9 +183,9 @@ class WenPai_Updater {
$info->tested = $response['tested'] ?? '';
$info->requires_php = $response['requires_php'] ?? '';
$info->last_updated = $response['last_updated'] ?? '';
$info->icons = $response['icons'] ?? array();
$info->banners = $response['banners'] ?? array();
$info->sections = $response['sections'] ?? array();
$info->icons = $response['icons'] ?? [];
$info->banners = $response['banners'] ?? [];
$info->sections = $response['sections'] ?? [];

return $info;
}
@ -203,12 +200,12 @@ class WenPai_Updater {
private function api_request( string $endpoint, ?array $body = null ) {
$url = self::API_URL . '/' . ltrim( $endpoint, '/' );

$args = array(
$args = [
'timeout' => 10,
'headers' => array(
'headers' => [
'Accept' => 'application/json',
),
);
],
];

if ( null !== $body ) {
$args['headers']['Content-Type'] = 'application/json';

File diff suppressed because it is too large Load diff

View file

@ -51,14 +51,11 @@ class WPSlug_Converter {
} catch (Exception $e) {
$execution_time = microtime(true) - $start_time;
$this->settings->updateConversionStats($mode, false, $execution_time);
$this->settings->logError(
'Conversion error in mode ' . $mode . ': ' . $e->getMessage(),
array(
$this->settings->logError('Conversion error in mode ' . $mode . ': ' . $e->getMessage(), array(
'text' => $text,
'mode' => $mode,
'options' => $options,
)
);
'options' => $options
));
return $this->cleanBasicSlug($text, $options);
}
@ -109,13 +106,10 @@ class WPSlug_Converter {
}

// 4. 调用 WPMind AI 进行语义化拼音转换
$result = wpmind_pinyin(
$text,
array(
$result = wpmind_pinyin($text, [
'context' => 'wpslug_semantic_pinyin',
'cache_ttl' => 604800, // 7天
)
);
]);

// 5. 处理结果
if (is_wp_error($result)) {
@ -276,7 +270,7 @@ class WPSlug_Converter {
'original' => $item,
'converted' => $converted,
'mode' => isset($options['conversion_mode']) ? $options['conversion_mode'] : 'pinyin',
'detected_language' => $this->detectLanguage( $item ),
'detected_language' => $this->detectLanguage($item)
);
} catch (Exception $e) {
$this->settings->logError('Batch convert error: ' . $e->getMessage());
@ -284,7 +278,7 @@ class WPSlug_Converter {
'original' => $item,
'converted' => sanitize_title($item),
'mode' => 'fallback',
'detected_language' => 'unknown',
'detected_language' => 'unknown'
);
}
}
@ -306,7 +300,7 @@ class WPSlug_Converter {
'converted' => $converted,
'detected_language' => $detected_lang,
'execution_time' => $execution_time,
'mode' => isset( $options['conversion_mode'] ) ? $options['conversion_mode'] : 'pinyin',
'mode' => isset($options['conversion_mode']) ? $options['conversion_mode'] : 'pinyin'
);
} catch (Exception $e) {
@ -319,7 +313,7 @@ class WPSlug_Converter {
'detected_language' => 'unknown',
'execution_time' => $execution_time,
'mode' => 'fallback',
'error' => $e->getMessage(),
'error' => $e->getMessage()
);
}
}
@ -339,7 +333,7 @@ class WPSlug_Converter {
'has_greek' => preg_match('/[\x{0370}-\x{03ff}]/u', $text) ? true : false,
'has_latin' => preg_match('/[a-zA-Z]/', $text) ? true : false,
'has_numbers' => preg_match('/\d/', $text) ? true : false,
'has_special_chars' => preg_match( '/[^\w\s\p{L}\p{N}]/u', $text ) ? true : false,
'has_special_chars' => preg_match('/[^\w\s\p{L}\p{N}]/u', $text) ? true : false
);
return $info;
@ -366,20 +360,20 @@ class WPSlug_Converter {
if (empty($text)) {
return array(
'valid' => false,
'error' => 'Input text is empty',
'error' => 'Input text is empty'
);
}
if ($max_length > 0 && mb_strlen($text, 'UTF-8') > $max_length) {
return array(
'valid' => false,
'error' => 'Input text exceeds maximum length of ' . $max_length . ' characters',
'error' => 'Input text exceeds maximum length of ' . $max_length . ' characters'
);
}
return array(
'valid' => true,
'message' => 'Input is valid',
'message' => 'Input is valid'
);
}
}

View file

@ -44,7 +44,8 @@ class WPSlug_Core {
/**
* Disabled: migrated to WPSlug_Updater (Update URI)
*/
private function init_update_checker() {
private function init_update_checker()
{
return;
}

@ -299,12 +300,10 @@ class WPSlug_Core {
if (!empty($new_slug) && $new_slug !== $post->post_name) {
$unique_slug = $this->optimizer->generateUniqueSlug($new_slug, $post->ID, $post->post_type);

wp_update_post(
array(
wp_update_post(array(
'ID' => $post->ID,
'post_name' => $unique_slug,
)
);
'post_name' => $unique_slug
));
}
} catch (Exception $e) {
$this->settings->logError('handlePostStatusTransition error: ' . $e->getMessage());

View file

@ -7,6 +7,7 @@ if ( ! defined( 'ABSPATH' ) ) {
class WPSlug_Optimizer {

public function __construct() {
}

public function optimize($text, $options = array()) {
@ -130,12 +131,9 @@ class WPSlug_Optimizer {
}

$words = preg_split('/[-\s_]+/', $text);
$words = array_filter(
$words,
function ( $word ) {
$words = array_filter($words, function($word) {
return !empty(trim($word));
}
);
});

if (count($words) <= $max_words) {
return $text;
@ -180,26 +178,20 @@ class WPSlug_Optimizer {
$suffix = 1;
while (true) {
$query = $wpdb->prepare(
"
$query = $wpdb->prepare("
SELECT ID FROM {$wpdb->posts}
WHERE post_name = %s
AND post_type = %s
AND ID != %d
LIMIT 1
",
$slug,
$post_type,
$post_id
);
", $slug, $post_type, $post_id);
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query is prepared above
if (!$wpdb->get_var($query)) {
break;
}
$slug = $original_slug . '-' . $suffix;
++$suffix;
$suffix++;
if ($suffix > 100) {
break;

View file

@ -48,7 +48,8 @@ class WPSlug_Pinyin {
$result .= $separator . strtolower($pinyin) . $separator;
}
}
} elseif ( $preserve_english && preg_match( '/[a-zA-Z]/', $char ) ) {
} else {
if ($preserve_english && preg_match('/[a-zA-Z]/', $char)) {
$result .= $char;
} elseif ($preserve_numbers && preg_match('/[0-9]/', $char)) {
$result .= $char;
@ -56,6 +57,7 @@ class WPSlug_Pinyin {
$result .= $separator;
}
}
}

if (!empty($separator)) {
$result = trim(preg_replace('/' . preg_quote($separator, '/') . '+/', $separator, $result), $separator);
@ -122,190 +124,43 @@ class WPSlug_Pinyin {
private function initFallbackDict() {
$this->pinyin_dict = array(
'一' => 'yi',
'二' => 'er',
'三' => 'san',
'四' => 'si',
'五' => 'wu',
'六' => 'liu',
'七' => 'qi',
'八' => 'ba',
'九' => 'jiu',
'十' => 'shi',
'的' => 'de',
'了' => 'le',
'是' => 'shi',
'我' => 'wo',
'你' => 'ni',
'他' => 'ta',
'她' => 'ta',
'它' => 'ta',
'好' => 'hao',
'很' => 'hen',
'都' => 'dou',
'会' => 'hui',
'个' => 'ge',
'这' => 'zhe',
'那' => 'na',
'中' => 'zhong',
'国' => 'guo',
'人' => 'ren',
'有' => 'you',
'来' => 'lai',
'可' => 'ke',
'以' => 'yi',
'上' => 'shang',
'下' => 'xia',
'大' => 'da',
'小' => 'xiao',
'多' => 'duo',
'少' => 'shao',
'什' => 'shen',
'么' => 'me',
'时' => 'shi',
'间' => 'jian',
'地' => 'di',
'方' => 'fang',
'年' => 'nian',
'月' => 'yue',
'日' => 'ri',
'天' => 'tian',
'水' => 'shui',
'火' => 'huo',
'木' => 'mu',
'金' => 'jin',
'土' => 'tu',
'山' => 'shan',
'海' => 'hai',
'河' => 'he',
'学' => 'xue',
'校' => 'xiao',
'老' => 'lao',
'师' => 'shi',
'生' => 'sheng',
'活' => 'huo',
'工' => 'gong',
'作' => 'zuo',
'家' => 'jia',
'庭' => 'ting',
'朋' => 'peng',
'友' => 'you',
'爱' => 'ai',
'情' => 'qing',
'心' => 'xin',
'想' => 'xiang',
'知' => 'zhi',
'道' => 'dao',
'看' => 'kan',
'见' => 'jian',
'听' => 'ting',
'说' => 'shuo',
'话' => 'hua',
'言' => 'yan',
'文' => 'wen',
'字' => 'zi',
'书' => 'shu',
'读' => 'du',
'写' => 'xie',
'画' => 'hua',
'吃' => 'chi',
'喝' => 'he',
'睡' => 'shui',
'觉' => 'jue',
'走' => 'zou',
'跑' => 'pao',
'飞' => 'fei',
'坐' => 'zuo',
'站' => 'zhan',
'躺' => 'tang',
'笑' => 'xiao',
'哭' => 'ku',
'高' => 'gao',
'兴' => 'xing',
'快' => 'kuai',
'乐' => 'le',
'难' => 'nan',
'过' => 'guo',
'新' => 'xin',
'旧' => 'jiu',
'长' => 'chang',
'短' => 'duan',
'宽' => 'kuan',
'窄' => 'zhai',
'厚' => 'hou',
'薄' => 'bao',
'深' => 'shen',
'浅' => 'qian',
'远' => 'yuan',
'近' => 'jin',
'美' => 'mei',
'丽' => 'li',
'漂' => 'piao',
'亮' => 'liang',
'帅' => 'shuai',
'聪' => 'cong',
'明' => 'ming',
'笨' => 'ben',
'懒' => 'lan',
'勤' => 'qin',
'忙' => 'mang',
'闲' => 'xian',
'累' => 'lei',
'轻' => 'qing',
'重' => 'zhong',
'松' => 'song',
'紧' => 'jin',
'开' => 'kai',
'关' => 'guan',
'门' => 'men',
'窗' => 'chuang',
'户' => 'hu',
'房' => 'fang',
'子' => 'zi',
'屋' => 'wu',
'楼' => 'lou',
'层' => 'ceng',
'街' => 'jie',
'路' => 'lu',
'桥' => 'qiao',
'车' => 'che',
'船' => 'chuan',
'机' => 'ji',
'电' => 'dian',
'话' => 'hua',
'视' => 'shi',
'脑' => 'nao',
'手' => 'shou',
'网' => 'wang',
'络' => 'luo',
'游' => 'you',
'戏' => 'xi',
'音' => 'yin',
'影' => 'ying',
'唱' => 'chang',
'歌' => 'ge',
'跳' => 'tiao',
'舞' => 'wu',
'购' => 'gou',
'物' => 'wu',
'买' => 'mai',
'卖' => 'mai',
'钱' => 'qian',
'价' => 'jia',
'格' => 'ge',
'便' => 'bian',
'宜' => 'yi',
'贵' => 'gui',
'医' => 'yi',
'院' => 'yuan',
'病' => 'bing',
'痛' => 'tong',
'健' => 'jian',
'康' => 'kang',
'运' => 'yun',
'动' => 'dong',
'锻' => 'duan',
'炼' => 'lian',
'一' => 'yi', '二' => 'er', '三' => 'san', '四' => 'si', '五' => 'wu',
'六' => 'liu', '七' => 'qi', '八' => 'ba', '九' => 'jiu', '十' => 'shi',
'的' => 'de', '了' => 'le', '是' => 'shi', '我' => 'wo', '你' => 'ni',
'他' => 'ta', '她' => 'ta', '它' => 'ta', '好' => 'hao', '很' => 'hen',
'都' => 'dou', '会' => 'hui', '个' => 'ge', '这' => 'zhe', '那' => 'na',
'中' => 'zhong', '国' => 'guo', '人' => 'ren', '有' => 'you', '来' => 'lai',
'可' => 'ke', '以' => 'yi', '上' => 'shang', '下' => 'xia', '大' => 'da',
'小' => 'xiao', '多' => 'duo', '少' => 'shao', '什' => 'shen', '么' => 'me',
'时' => 'shi', '间' => 'jian', '地' => 'di', '方' => 'fang', '年' => 'nian',
'月' => 'yue', '日' => 'ri', '天' => 'tian', '水' => 'shui', '火' => 'huo',
'木' => 'mu', '金' => 'jin', '土' => 'tu', '山' => 'shan', '海' => 'hai',
'河' => 'he', '学' => 'xue', '校' => 'xiao', '老' => 'lao', '师' => 'shi',
'生' => 'sheng', '活' => 'huo', '工' => 'gong', '作' => 'zuo', '家' => 'jia',
'庭' => 'ting', '朋' => 'peng', '友' => 'you', '爱' => 'ai', '情' => 'qing',
'心' => 'xin', '想' => 'xiang', '知' => 'zhi', '道' => 'dao', '看' => 'kan',
'见' => 'jian', '听' => 'ting', '说' => 'shuo', '话' => 'hua', '言' => 'yan',
'文' => 'wen', '字' => 'zi', '书' => 'shu', '读' => 'du', '写' => 'xie',
'画' => 'hua', '吃' => 'chi', '喝' => 'he', '睡' => 'shui', '觉' => 'jue',
'走' => 'zou', '跑' => 'pao', '飞' => 'fei', '坐' => 'zuo', '站' => 'zhan',
'躺' => 'tang', '笑' => 'xiao', '哭' => 'ku', '高' => 'gao', '兴' => 'xing',
'快' => 'kuai', '乐' => 'le', '难' => 'nan', '过' => 'guo', '新' => 'xin',
'旧' => 'jiu', '长' => 'chang', '短' => 'duan', '宽' => 'kuan', '窄' => 'zhai',
'厚' => 'hou', '薄' => 'bao', '深' => 'shen', '浅' => 'qian', '远' => 'yuan',
'近' => 'jin', '美' => 'mei', '丽' => 'li', '漂' => 'piao', '亮' => 'liang',
'帅' => 'shuai', '聪' => 'cong', '明' => 'ming', '笨' => 'ben', '懒' => 'lan',
'勤' => 'qin', '忙' => 'mang', '闲' => 'xian', '累' => 'lei', '轻' => 'qing',
'重' => 'zhong', '松' => 'song', '紧' => 'jin', '开' => 'kai', '关' => 'guan',
'门' => 'men', '窗' => 'chuang', '户' => 'hu', '房' => 'fang', '子' => 'zi',
'屋' => 'wu', '楼' => 'lou', '层' => 'ceng', '街' => 'jie', '路' => 'lu',
'桥' => 'qiao', '车' => 'che', '船' => 'chuan', '机' => 'ji', '电' => 'dian',
'话' => 'hua', '视' => 'shi', '脑' => 'nao', '手' => 'shou', '网' => 'wang',
'络' => 'luo', '游' => 'you', '戏' => 'xi', '音' => 'yin', '影' => 'ying',
'唱' => 'chang', '歌' => 'ge', '跳' => 'tiao', '舞' => 'wu', '购' => 'gou',
'物' => 'wu', '买' => 'mai', '卖' => 'mai', '钱' => 'qian', '价' => 'jia',
'格' => 'ge', '便' => 'bian', '宜' => 'yi', '贵' => 'gui', '医' => 'yi',
'院' => 'yuan', '病' => 'bing', '痛' => 'tong', '健' => 'jian', '康' => 'kang',
'运' => 'yun', '动' => 'dong', '锻' => 'duan', '炼' => 'lian'
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -72,19 +72,16 @@ class WPSlug_Translator {
'q' => $text,
'source' => $source_lang,
'target' => $target_lang,
'format' => 'text',
'format' => 'text'
);

$response = wp_remote_post(
$url,
array(
$response = wp_remote_post($url, array(
'timeout' => 15,
'body' => $params,
'headers' => array(
'User-Agent' => 'WPSlug/' . WPSLUG_VERSION,
),
'User-Agent' => 'WPSlug/' . WPSLUG_VERSION
)
);
));

if (is_wp_error($response)) {
if (isset($options['debug_mode']) && $options['debug_mode']) {
@ -143,19 +140,16 @@ class WPSlug_Translator {
'to' => $target_lang,
'appid' => $app_id,
'salt' => $salt,
'sign' => $sign,
'sign' => $sign
);

$response = wp_remote_post(
$url,
array(
$response = wp_remote_post($url, array(
'timeout' => 15,
'body' => $params,
'headers' => array(
'User-Agent' => 'WPSlug/' . WPSLUG_VERSION,
),
'User-Agent' => 'WPSlug/' . WPSLUG_VERSION
)
);
));

if (is_wp_error($response)) {
if (isset($options['debug_mode']) && $options['debug_mode']) {
@ -247,18 +241,13 @@ class WPSlug_Translator {
// 5. 调用 WPMind API语言设置已在前面获取
$start_time = microtime(true);
$result = wpmind_translate(
$text,
$source_lang,
$target_lang,
array(
$result = wpmind_translate($text, $source_lang, $target_lang, [
'context' => 'wpslug_translation',
'format' => 'slug',
'cache_ttl' => 86400, // WPMind 内部缓存 1 天
'max_tokens' => 100,
'temperature' => 0.3,
)
);
]);

$elapsed_time = round((microtime(true) - $start_time) * 1000);

@ -326,7 +315,7 @@ class WPSlug_Translator {
$results[] = array(
'original' => $item,
'translated' => $translated,
'service' => isset( $options['translation_service'] ) ? $options['translation_service'] : 'none',
'service' => isset($options['translation_service']) ? $options['translation_service'] : 'none'
);
} catch (Exception $e) {
if (isset($options['debug_mode']) && $options['debug_mode']) {
@ -335,7 +324,7 @@ class WPSlug_Translator {
$results[] = array(
'original' => $item,
'translated' => sanitize_title($item),
'service' => 'fallback',
'service' => 'fallback'
);
}
}

View file

@ -106,287 +106,73 @@ class WPSlug_Transliterator {

private function initCharMaps() {
$this->char_maps = array(
'А' => 'A',
'а' => 'a',
'Б' => 'B',
'б' => 'b',
'В' => 'V',
'в' => 'v',
'Г' => 'G',
'г' => 'g',
'Д' => 'D',
'д' => 'd',
'Е' => 'E',
'е' => 'e',
'Ё' => 'Yo',
'ё' => 'yo',
'Ж' => 'Zh',
'ж' => 'zh',
'З' => 'Z',
'з' => 'z',
'И' => 'I',
'и' => 'i',
'Й' => 'J',
'й' => 'j',
'К' => 'K',
'к' => 'k',
'Л' => 'L',
'л' => 'l',
'М' => 'M',
'м' => 'm',
'Н' => 'N',
'н' => 'n',
'О' => 'O',
'о' => 'o',
'П' => 'P',
'п' => 'p',
'Р' => 'R',
'р' => 'r',
'С' => 'S',
'с' => 's',
'Т' => 'T',
'т' => 't',
'У' => 'U',
'у' => 'u',
'Ф' => 'F',
'ф' => 'f',
'Х' => 'H',
'х' => 'h',
'Ц' => 'C',
'ц' => 'c',
'Ч' => 'Ch',
'ч' => 'ch',
'Ш' => 'Sh',
'ш' => 'sh',
'Щ' => 'Shh',
'щ' => 'shh',
'Ъ' => '',
'ъ' => '',
'Ы' => 'Y',
'ы' => 'y',
'Ь' => '',
'ь' => '',
'Э' => 'E',
'э' => 'e',
'Ю' => 'Yu',
'ю' => 'yu',
'Я' => 'Ya',
'я' => 'ya',
'А' => 'A', 'а' => 'a', 'Б' => 'B', 'б' => 'b', 'В' => 'V', 'в' => 'v',
'Г' => 'G', 'г' => 'g', 'Д' => 'D', 'д' => 'd', 'Е' => 'E', 'е' => 'e',
'Ё' => 'Yo', 'ё' => 'yo', 'Ж' => 'Zh', 'ж' => 'zh', 'З' => 'Z', 'з' => 'z',
'И' => 'I', 'и' => 'i', 'Й' => 'J', 'й' => 'j', 'К' => 'K', 'к' => 'k',
'Л' => 'L', 'л' => 'l', 'М' => 'M', 'м' => 'm', 'Н' => 'N', 'н' => 'n',
'О' => 'O', 'о' => 'o', 'П' => 'P', 'п' => 'p', 'Р' => 'R', 'р' => 'r',
'С' => 'S', 'с' => 's', 'Т' => 'T', 'т' => 't', 'У' => 'U', 'у' => 'u',
'Ф' => 'F', 'ф' => 'f', 'Х' => 'H', 'х' => 'h', 'Ц' => 'C', 'ц' => 'c',
'Ч' => 'Ch', 'ч' => 'ch', 'Ш' => 'Sh', 'ш' => 'sh', 'Щ' => 'Shh', 'щ' => 'shh',
'Ъ' => '', 'ъ' => '', 'Ы' => 'Y', 'ы' => 'y', 'Ь' => '', 'ь' => '',
'Э' => 'E', 'э' => 'e', 'Ю' => 'Yu', 'ю' => 'yu', 'Я' => 'Ya', 'я' => 'ya',
'ä' => 'ae',
'ö' => 'oe',
'ü' => 'ue',
'ß' => 'ss',
'Ä' => 'Ae',
'Ö' => 'Oe',
'Ü' => 'Ue',
'ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 'ß' => 'ss',
'Ä' => 'Ae', 'Ö' => 'Oe', 'Ü' => 'Ue',
'à' => 'a',
'á' => 'a',
'â' => 'a',
'ã' => 'a',
'ä' => 'a',
'å' => 'a',
'À' => 'A',
'Á' => 'A',
'Â' => 'A',
'Ã' => 'A',
'Ä' => 'A',
'Å' => 'A',
'è' => 'e',
'é' => 'e',
'ê' => 'e',
'ë' => 'e',
'È' => 'E',
'É' => 'E',
'Ê' => 'E',
'Ë' => 'E',
'ì' => 'i',
'í' => 'i',
'î' => 'i',
'ï' => 'i',
'Ì' => 'I',
'Í' => 'I',
'Î' => 'I',
'Ï' => 'I',
'ò' => 'o',
'ó' => 'o',
'ô' => 'o',
'õ' => 'o',
'ö' => 'o',
'ø' => 'o',
'Ò' => 'O',
'Ó' => 'O',
'Ô' => 'O',
'Õ' => 'O',
'Ö' => 'O',
'Ø' => 'O',
'ù' => 'u',
'ú' => 'u',
'û' => 'u',
'ü' => 'u',
'Ù' => 'U',
'Ú' => 'U',
'Û' => 'U',
'Ü' => 'U',
'ý' => 'y',
'ÿ' => 'y',
'Ý' => 'Y',
'Ÿ' => 'Y',
'ñ' => 'n',
'Ñ' => 'N',
'ç' => 'c',
'Ç' => 'C',
'æ' => 'ae',
'Æ' => 'AE',
'œ' => 'oe',
'Œ' => 'OE',
'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a',
'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A',
'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E',
'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i',
'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I',
'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ø' => 'o',
'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ø' => 'O',
'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u',
'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U',
'ý' => 'y', 'ÿ' => 'y', 'Ý' => 'Y', 'Ÿ' => 'Y',
'ñ' => 'n', 'Ñ' => 'N',
'ç' => 'c', 'Ç' => 'C',
'æ' => 'ae', 'Æ' => 'AE',
'œ' => 'oe', 'Œ' => 'OE',
'ā' => 'a',
'ē' => 'e',
'ī' => 'i',
'ō' => 'o',
'ū' => 'u',
'Ā' => 'A',
'Ē' => 'E',
'Ī' => 'I',
'Ō' => 'O',
'Ū' => 'U',
'ā' => 'a', 'ē' => 'e', 'ī' => 'i', 'ō' => 'o', 'ū' => 'u',
'Ā' => 'A', 'Ē' => 'E', 'Ī' => 'I', 'Ō' => 'O', 'Ū' => 'U',
'ą' => 'a',
'ć' => 'c',
'ę' => 'e',
'ł' => 'l',
'ń' => 'n',
'ó' => 'o',
'ś' => 's',
'ź' => 'z',
'ż' => 'z',
'Ą' => 'A',
'Ć' => 'C',
'Ę' => 'E',
'Ł' => 'L',
'Ń' => 'N',
'Ó' => 'O',
'Ś' => 'S',
'Ź' => 'Z',
'Ż' => 'Z',
'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n',
'ó' => 'o', 'ś' => 's', 'ź' => 'z', 'ż' => 'z',
'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'E', 'Ł' => 'L', 'Ń' => 'N',
'Ó' => 'O', 'Ś' => 'S', 'Ź' => 'Z', 'Ż' => 'Z',
'ă' => 'a',
'î' => 'i',
'ș' => 's',
'ț' => 't',
'Ă' => 'A',
'Î' => 'I',
'Ș' => 'S',
'Ț' => 'T',
'ă' => 'a', 'î' => 'i', 'ș' => 's', 'ț' => 't',
'Ă' => 'A', 'Î' => 'I', 'Ș' => 'S', 'Ț' => 'T',
'α' => 'a',
'β' => 'b',
'γ' => 'g',
'δ' => 'd',
'ε' => 'e',
'ζ' => 'z',
'η' => 'i',
'θ' => 'th',
'ι' => 'i',
'κ' => 'k',
'λ' => 'l',
'μ' => 'm',
'ν' => 'n',
'ξ' => 'x',
'ο' => 'o',
'π' => 'p',
'ρ' => 'r',
'σ' => 's',
'τ' => 't',
'υ' => 'y',
'φ' => 'f',
'χ' => 'ch',
'ψ' => 'ps',
'ω' => 'o',
'Α' => 'A',
'Β' => 'B',
'Γ' => 'G',
'Δ' => 'D',
'Ε' => 'E',
'Ζ' => 'Z',
'Η' => 'I',
'Θ' => 'TH',
'Ι' => 'I',
'Κ' => 'K',
'Λ' => 'L',
'Μ' => 'M',
'Ν' => 'N',
'Ξ' => 'X',
'Ο' => 'O',
'Π' => 'P',
'Ρ' => 'R',
'Σ' => 'S',
'Τ' => 'T',
'Υ' => 'Y',
'Φ' => 'F',
'Χ' => 'CH',
'Ψ' => 'PS',
'Ω' => 'O',
'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e',
'ζ' => 'z', 'η' => 'i', 'θ' => 'th', 'ι' => 'i', 'κ' => 'k',
'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => 'x', 'ο' => 'o',
'π' => 'p', 'ρ' => 'r', 'σ' => 's', 'τ' => 't', 'υ' => 'y',
'φ' => 'f', 'χ' => 'ch', 'ψ' => 'ps', 'ω' => 'o',
'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E',
'Ζ' => 'Z', 'Η' => 'I', 'Θ' => 'TH', 'Ι' => 'I', 'Κ' => 'K',
'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => 'X', 'Ο' => 'O',
'Π' => 'P', 'Ρ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y',
'Φ' => 'F', 'Χ' => 'CH', 'Ψ' => 'PS', 'Ω' => 'O',
'ã' => 'a',
'õ' => 'o',
'ç' => 'c',
'Ã' => 'A',
'Õ' => 'O',
'Ç' => 'C',
'ã' => 'a', 'õ' => 'o', 'ç' => 'c',
'Ã' => 'A', 'Õ' => 'O', 'Ç' => 'C',
'ğ' => 'g',
'ı' => 'i',
'ş' => 's',
'ü' => 'u',
'ö' => 'o',
'ç' => 'c',
'Ğ' => 'G',
'İ' => 'I',
'Ş' => 'S',
'Ü' => 'U',
'Ö' => 'O',
'Ç' => 'C',
'ğ' => 'g', 'ı' => 'i', 'ş' => 's', 'ü' => 'u', 'ö' => 'o', 'ç' => 'c',
'Ğ' => 'G', 'İ' => 'I', 'Ş' => 'S', 'Ü' => 'U', 'Ö' => 'O', 'Ç' => 'C',
'ک' => 'k',
'گ' => 'g',
'چ' => 'ch',
'پ' => 'p',
'ژ' => 'zh',
'ی' => 'y',
'ء' => 'a',
'ؤ' => 'w',
'ئ' => 'y',
'ة' => 'h',
'ا' => 'a',
'ب' => 'b',
'ت' => 't',
'ث' => 'th',
'ج' => 'j',
'ح' => 'h',
'خ' => 'kh',
'د' => 'd',
'ذ' => 'dh',
'ر' => 'r',
'ز' => 'z',
'س' => 's',
'ش' => 'sh',
'ص' => 's',
'ض' => 'd',
'ط' => 't',
'ظ' => 'dh',
'ع' => 'a',
'غ' => 'gh',
'ف' => 'f',
'ق' => 'q',
'ل' => 'l',
'م' => 'm',
'ن' => 'n',
'ه' => 'h',
'و' => 'w',
'ي' => 'y',
'ک' => 'k', 'گ' => 'g', 'چ' => 'ch', 'پ' => 'p', 'ژ' => 'zh',
'ی' => 'y', 'ء' => 'a', 'ؤ' => 'w', 'ئ' => 'y', 'ة' => 'h',
'ا' => 'a', 'ب' => 'b', 'ت' => 't', 'ث' => 'th', 'ج' => 'j',
'ح' => 'h', 'خ' => 'kh', 'د' => 'd', 'ذ' => 'dh', 'ر' => 'r',
'ز' => 'z', 'س' => 's', 'ش' => 'sh', 'ص' => 's', 'ض' => 'd',
'ط' => 't', 'ظ' => 'dh', 'ع' => 'a', 'غ' => 'gh', 'ف' => 'f',
'ق' => 'q', 'ل' => 'l', 'م' => 'm', 'ن' => 'n', 'ه' => 'h',
'و' => 'w', 'ي' => 'y'
);
}


View file

@ -87,39 +87,9 @@ class WPSlug_Validator {
public static function validateLanguageCode($code) {
$valid_codes = array(
'auto',
'zh',
'zh-TW',
'en',
'es',
'fr',
'de',
'ja',
'ko',
'ru',
'ar',
'it',
'pt',
'nl',
'pl',
'tr',
'sv',
'da',
'no',
'fi',
'cs',
'hu',
'ro',
'bg',
'hr',
'sk',
'sl',
'et',
'lv',
'lt',
'mt',
'el',
'cy',
'auto', 'zh', 'zh-TW', 'en', 'es', 'fr', 'de', 'ja', 'ko', 'ru',
'ar', 'it', 'pt', 'nl', 'pl', 'tr', 'sv', 'da', 'no', 'fi', 'cs',
'hu', 'ro', 'bg', 'hr', 'sk', 'sl', 'et', 'lv', 'lt', 'mt', 'el', 'cy'
);
return in_array($code, $valid_codes, true) ? $code : 'auto';
@ -207,7 +177,7 @@ class WPSlug_Validator {
'php_version' => version_compare(PHP_VERSION, '7.0', '>='),
'wordpress_version' => version_compare(get_bloginfo('version'), '5.0', '>='),
'mbstring_extension' => extension_loaded('mbstring'),
'json_extension' => extension_loaded( 'json' ),
'json_extension' => extension_loaded('json')
);
$errors = array();

View file

@ -1,39 +0,0 @@
<?xml version="1.0"?>
<ruleset name="WPSlug">
<description>WPSlug 项目代码规范</description>

<file>.</file>
<arg name="extensions" value="php"/>
<arg name="warning-severity" value="0"/>
<exclude-pattern>vendor/*</exclude-pattern>
<exclude-pattern>node_modules/*</exclude-pattern>
<exclude-pattern>tests/*</exclude-pattern>
<exclude-pattern>lib/*</exclude-pattern>
<exclude-pattern>languages/*</exclude-pattern>

<rule ref="WordPress-Extra">
<!-- 项目使用 camelCase 方法名 -->
<exclude name="WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid"/>
<!-- 不强制 Yoda 条件 -->
<exclude name="WordPress.PHP.YodaConditions.NotYoda"/>
<!-- 允许 error_log -->
<exclude name="WordPress.PHP.DevelopmentFunctions.error_log_error_log"/>
<!-- 允许短三元运算符 -->
<exclude name="Universal.Operators.DisallowShortTernary.Found"/>
<!-- _e() 是 WordPress 标准翻译函数 -->
<exclude name="WordPress.Security.EscapeOutput.UnsafePrintingFunction"/>
<!-- 拼音字典允许重复键 -->
<exclude name="Universal.Arrays.DuplicateArrayKey.Found"/>
<!-- Nonce 验证由其他机制处理 -->
<exclude name="WordPress.Security.NonceVerification.Recommended"/>
<exclude name="WordPress.Security.NonceVerification.Missing"/>
<!-- 主文件结构是 WordPress 插件标准格式 -->
<exclude name="WordPress.Files.FileName.InvalidClassFileName"/>
<exclude name="Universal.Files.SeparateFunctionsFromOO.Mixed"/>
</rule>

<!-- 输出转义降为 warning不阻断 CI -->
<rule ref="WordPress.Security.EscapeOutput.OutputNotEscaped">
<type>warning</type>
</rule>
</ruleset>

View file

@ -15,37 +15,40 @@ Requires at least: 6.0
Requires PHP: 7.4
*/

if ( ! defined( 'ABSPATH' ) ) {
if (!defined("ABSPATH")) {
exit();
}

define( 'WPSLUG_VERSION', '1.1.0' );
define( 'WPSLUG_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'WPSLUG_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'WPSLUG_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );

class WPSlug {
define("WPSLUG_VERSION", "1.1.0");
define("WPSLUG_PLUGIN_DIR", plugin_dir_path(__FILE__));
define("WPSLUG_PLUGIN_URL", plugin_dir_url(__FILE__));
define("WPSLUG_PLUGIN_BASENAME", plugin_basename(__FILE__));

class WPSlug
{
private static $instance = null;
private $core = null;

public static function getInstance() {
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}

private function __construct() {
add_action( 'plugins_loaded', array( $this, 'loadPlugin' ) );
register_activation_hook( __FILE__, array( $this, 'activate' ) );
register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );
register_uninstall_hook( __FILE__, array( 'WPSlug', 'uninstall' ) );
private function __construct()
{
add_action("plugins_loaded", [$this, "loadPlugin"]);
register_activation_hook(__FILE__, [$this, "activate"]);
register_deactivation_hook(__FILE__, [$this, "deactivate"]);
register_uninstall_hook(__FILE__, ["WPSlug", "uninstall"]);
add_action( 'init', array( $this, 'initLanguages' ) );
add_action('init', [$this, 'initLanguages']);
}

public function loadPlugin() {
public function loadPlugin()
{
if (!$this->checkRequirements()) {
return;
}
@ -56,51 +59,48 @@ class WPSlug {
new WenPai_Updater( WPSLUG_PLUGIN_BASENAME, WPSLUG_VERSION );
}

private function checkRequirements() {
if ( version_compare( PHP_VERSION, '7.0', '<' ) ) {
add_action(
'admin_notices',
function () {
private function checkRequirements()
{
if (version_compare(PHP_VERSION, "7.0", "<")) {
add_action('admin_notices', function() {
echo '<div class="notice notice-error"><p>';
echo esc_html__('WP Slug requires PHP 7.0 or higher. Please upgrade your PHP version.', 'wpslug');
echo '</p></div>';
}
);
});
return false;
}

if ( version_compare( get_bloginfo( 'version' ), '5.0', '<' ) ) {
add_action(
'admin_notices',
function () {
if (version_compare(get_bloginfo("version"), "5.0", "<")) {
add_action('admin_notices', function() {
echo '<div class="notice notice-error"><p>';
echo esc_html__('WP Slug requires WordPress 5.0 or higher. Please upgrade your WordPress version.', 'wpslug');
echo '</p></div>';
}
);
});
return false;
}

return true;
}

private function loadDependencies() {
require_once WPSLUG_PLUGIN_DIR . 'includes/class-wpslug-validator.php';
require_once WPSLUG_PLUGIN_DIR . 'includes/class-wpslug-settings.php';
require_once WPSLUG_PLUGIN_DIR . 'includes/class-wpslug-pinyin.php';
require_once WPSLUG_PLUGIN_DIR . 'includes/class-wpslug-optimizer.php';
require_once WPSLUG_PLUGIN_DIR . 'includes/class-wpslug-transliterator.php';
require_once WPSLUG_PLUGIN_DIR . 'includes/class-wpslug-translator.php';
require_once WPSLUG_PLUGIN_DIR . 'includes/class-wpslug-converter.php';
require_once WPSLUG_PLUGIN_DIR . 'includes/class-wpslug-core.php';
require_once WPSLUG_PLUGIN_DIR . 'includes/class-wenpai-updater.php';
private function loadDependencies()
{
require_once WPSLUG_PLUGIN_DIR . "includes/class-wpslug-validator.php";
require_once WPSLUG_PLUGIN_DIR . "includes/class-wpslug-settings.php";
require_once WPSLUG_PLUGIN_DIR . "includes/class-wpslug-pinyin.php";
require_once WPSLUG_PLUGIN_DIR . "includes/class-wpslug-optimizer.php";
require_once WPSLUG_PLUGIN_DIR . "includes/class-wpslug-transliterator.php";
require_once WPSLUG_PLUGIN_DIR . "includes/class-wpslug-translator.php";
require_once WPSLUG_PLUGIN_DIR . "includes/class-wpslug-converter.php";
require_once WPSLUG_PLUGIN_DIR . "includes/class-wpslug-core.php";
require_once WPSLUG_PLUGIN_DIR . "includes/class-wenpai-updater.php";

if (is_admin()) {
require_once WPSLUG_PLUGIN_DIR . 'includes/class-wpslug-admin.php';
require_once WPSLUG_PLUGIN_DIR . "includes/class-wpslug-admin.php";
}
}

public function initLanguages() {
public function initLanguages()
{
$locale = apply_filters('plugin_locale', get_locale(), 'wpslug');
$mo_file = WPSLUG_PLUGIN_DIR . "languages/wpslug-{$locale}.mo";
@ -109,17 +109,19 @@ class WPSlug {
}
}

public function loadTextdomain() {
public function loadTextdomain()
{
load_plugin_textdomain(
'wpslug',
"wpslug",
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages/'
dirname(plugin_basename(__FILE__)) . "/languages/"
);
}

public function activate() {
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
public function activate()
{
if (!function_exists("is_plugin_active")) {
require_once ABSPATH . "wp-admin/includes/plugin.php";
}

if (!$this->checkRequirements()) {
@ -152,7 +154,8 @@ class WPSlug {
}
}

public function deactivate() {
public function deactivate()
{
try {
if ($this->core) {
$this->core->deactivate();
@ -165,9 +168,10 @@ class WPSlug {
}
}

public static function uninstall() {
public static function uninstall()
{
try {
if ( class_exists( 'WPSlug_Settings' ) ) {
if (class_exists("WPSlug_Settings")) {
$settings = new WPSlug_Settings();
$settings->uninstall();
}
@ -179,25 +183,29 @@ class WPSlug {
}
}

public function getCore() {
public function getCore()
{
return $this->core;
}

public function getSettings() {
public function getSettings()
{
if ($this->core) {
return $this->core->getSettings();
}
return null;
}

public function getConverter() {
public function getConverter()
{
if ($this->core) {
return $this->core->getConverter();
}
return null;
}

public function getOptimizer() {
public function getOptimizer()
{
if ($this->core) {
return $this->core->getOptimizer();
}
@ -205,14 +213,13 @@ class WPSlug {
}
}

function wpslug() {
function wpslug()
{
return WPSlug::getInstance();
}

if (is_admin()) {
add_action(
'admin_init',
function () {
add_action('admin_init', function() {
if (get_option('wpslug_activation_redirect', false)) {
delete_option('wpslug_activation_redirect');
if (!isset($_GET['activate-multi'])) {
@ -220,8 +227,7 @@ if ( is_admin() ) {
exit;
}
}
}
);
});
}

wpslug();