wpbridge/includes/Commercial/Vendors/WooCommerceVendor.php
feibisi 39b30af242 feat(commercial): add vendor system for third-party GPL plugin sources
- Add VendorInterface, AbstractVendor, VendorManager for vendor abstraction
- Add WooCommerceVendor for WooCommerce API Manager integration
- Add BridgeManager with hybrid mode (official + vendor + custom sources)
- Add GPLValidator for GPL compliance checking
- Add LicenseProxy for license key management
- Add VendorAdmin with AJAX handlers for vendor CRUD operations
- Add vendors.php admin tab with stats, vendor list, custom plugins
- Add vendor management CSS styles and JS module
- Update main.php to include vendors tab

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-15 08:39:37 +08:00

465 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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