wpbridge/includes/FAIR/FairProtocol.php
feibisi 76c0053f71 feat: 实现方案 B 管理界面和 FAIR 协议支持
任务 #43 - 重构管理界面为项目优先:
- 新增"项目"Tab,显示已安装插件/主题列表
- 子 Tab 结构:插件 / 主题 / 默认规则
- 支持为每个项目配置更新源(默认/禁用/自定义)
- 支持批量操作(设置源/重置/禁用)
- 支持搜索过滤
- 默认规则配置界面(全局/插件/主题)

任务 #44 - 添加 FAIR 源类型支持:
- 新增 FairProtocol 类:DID 解析、ED25519 签名验证
- 新增 FairSourceAdapter 类:FAIR 源更新检查和下载
- 支持 did:fair: 格式的 DID 标识符
- 支持 sodium 扩展和 paragonie/sodium_compat 回退
- 包签名验证和文件哈希校验

新增文件:
- includes/FAIR/FairProtocol.php
- includes/FAIR/FairSourceAdapter.php
- templates/admin/tabs/projects.php
- templates/admin/partials/project-list-plugins.php
- templates/admin/partials/project-list-themes.php
- templates/admin/partials/defaults-config.php

修改文件:
- includes/Admin/AdminPage.php - 添加项目配置 AJAX 处理
- templates/admin/main.php - 添加"项目"Tab
- assets/css/admin.css - 项目列表样式
- assets/js/admin.js - 项目管理 JavaScript

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:38:32 +08:00

322 lines
9.1 KiB
PHP

<?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;
}
}