wpmind/includes/class-wenpai-license.php
LinuxJoy adb6c49301 style: phpcbf 自动格式化 — 全量 WPCS 3.x 规范对齐
135 文件 19,211 处自动修复:空格→Tab 缩进、括号间距、
函数声明空格、前置自增、尾逗号等纯格式化改动。
零逻辑变更,php -l + token 级对比验证通过。

新增 phpcs.xml.dist / phpstan.neon.dist 项目配置。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 00:48:46 +08:00

323 lines
6.7 KiB
PHP

<?php
/**
* WenPai License Client
*
* Handles license verification, activation, and deactivation
* for WenPai commercial plugins.
*
* @package WPMind
* @since 4.0.0
*/
declare( strict_types=1 );
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WenPai License client.
*
* @since 4.0.0
*/
class WenPai_License {
/**
* License server API URL.
*
* @var string
*/
private string $api_url = 'https://license.wenpai.net';
/**
* Product slug.
*
* @var string
*/
private string $product_slug;
/**
* WordPress option key for the license key.
*
* @var string
*/
private string $option_key;
/**
* Transient key for cached verification.
*
* @var string
*/
private string $cache_key;
/**
* Constructor.
*
* @param string $product_slug Product identifier.
* @param string $api_url Optional custom API URL.
*/
public function __construct( string $product_slug, string $api_url = '' ) {
$this->product_slug = $product_slug;
$this->option_key = 'wenpai_license_key_' . $product_slug;
$this->cache_key = 'wenpai_license_' . $product_slug;
if ( $api_url !== '' ) {
$this->api_url = rtrim( $api_url, '/' );
} elseif ( defined( 'WENPAI_LICENSE_URL' ) ) {
$this->api_url = rtrim( WENPAI_LICENSE_URL, '/' );
}
}
/**
* Get the stored license key.
*
* @return string
*/
public function get_key(): string {
return (string) get_option( $this->option_key, '' );
}
/**
* Save a license key.
*
* @param string $key License key.
*/
public function set_key( string $key ): void {
update_option( $this->option_key, sanitize_text_field( $key ) );
delete_transient( $this->cache_key );
}
/**
* Verify the license remotely. Results are cached for 24 hours.
*
* @param bool $force_refresh Skip cache.
* @return array{valid: bool, plan: string, expires_at: string|null, features: array, cache_ttl: int}
*/
public function verify( bool $force_refresh = false ): array {
$key = $this->get_key();
if ( $key === '' ) {
return $this->free_response();
}
if ( ! $force_refresh ) {
$cached = get_transient( $this->cache_key );
if ( is_array( $cached ) ) {
return $cached;
}
}
$response = $this->api_request(
'/api/v1/license/verify',
[
'license_key' => $key,
'site_url' => home_url(),
]
);
if ( is_wp_error( $response ) ) {
// Grace period: use last known good state for up to 7 days.
$grace = get_option( $this->cache_key . '_grace' );
if ( is_array( $grace ) ) {
$grace_expires = (int) ( $grace['_grace_until'] ?? 0 );
if ( $grace_expires > time() ) {
return $grace;
}
}
return $this->free_response();
}
$data = $this->parse_response( $response );
// Cache the result.
$ttl = (int) ( $data['cache_ttl'] ?? 86400 );
set_transient( $this->cache_key, $data, $ttl );
// Store grace period copy (7 days).
$data['_grace_until'] = time() + ( 7 * DAY_IN_SECONDS );
update_option( $this->cache_key . '_grace', $data );
unset( $data['_grace_until'] );
return $data;
}
/**
* Activate the current site.
*
* @return bool True on success.
*/
public function activate(): bool {
$key = $this->get_key();
if ( $key === '' ) {
return false;
}
$response = $this->api_request(
'/api/v1/license/activate',
[
'license_key' => $key,
'site_url' => home_url(),
]
);
if ( is_wp_error( $response ) ) {
return false;
}
$data = $this->parse_response( $response );
// Refresh cache with activation result.
if ( ! empty( $data['valid'] ) ) {
$ttl = (int) ( $data['cache_ttl'] ?? 86400 );
set_transient( $this->cache_key, $data, $ttl );
}
return ! empty( $data['valid'] );
}
/**
* Deactivate the current site.
*
* @return bool True on success.
*/
public function deactivate(): bool {
$key = $this->get_key();
if ( $key === '' ) {
return false;
}
$response = $this->api_request(
'/api/v1/license/deactivate',
[
'license_key' => $key,
'site_url' => home_url(),
]
);
delete_transient( $this->cache_key );
delete_option( $this->cache_key . '_grace' );
return ! is_wp_error( $response );
}
/**
* Get the current plan name.
*
* @return string free|pro|enterprise
*/
public function plan(): string {
$data = $this->verify();
if ( empty( $data['valid'] ) ) {
return 'free';
}
return $data['plan'] ?? 'free';
}
/**
* Check if a specific feature is available.
*
* @param string $feature Feature key.
* @return bool
*/
public function can( string $feature ): bool {
$data = $this->verify();
if ( empty( $data['valid'] ) || empty( $data['features'] ) ) {
return false;
}
return isset( $data['features'][ $feature ] ) && $data['features'][ $feature ] !== 0;
}
/**
* Get the quota for a feature. Returns -1 for unlimited.
*
* @param string $feature Feature key.
* @return int
*/
public function quota( string $feature ): int {
$data = $this->verify();
if ( empty( $data['valid'] ) || empty( $data['features'] ) ) {
$free = $this->free_features();
return $free[ $feature ] ?? 0;
}
return (int) ( $data['features'][ $feature ] ?? 0 );
}
/**
* Check if the license is valid (includes 7-day grace period).
*
* @return bool
*/
public function is_valid(): bool {
$data = $this->verify();
return ! empty( $data['valid'] );
}
/**
* Make an API request to the license server.
*
* @param string $endpoint API path.
* @param array $body Request body.
* @return array|\WP_Error
*/
private function api_request( string $endpoint, array $body ) {
$url = $this->api_url . $endpoint;
$response = wp_remote_post(
$url,
[
'timeout' => 15,
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( $body ),
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
if ( $code < 200 || $code >= 300 ) {
return new \WP_Error( 'license_api_error', 'License API returned ' . $code );
}
return $response;
}
/**
* Parse the JSON response body.
*
* @param array $response wp_remote_post response.
* @return array
*/
private function parse_response( $response ): array {
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
return is_array( $data ) ? $data : [];
}
/**
* Default response for free/unlicensed users.
*
* @return array
*/
private function free_response(): array {
return [
'valid' => false,
'plan' => 'free',
'features' => $this->free_features(),
'cache_ttl' => 3600,
];
}
/**
* Free tier feature limits.
*
* @return array<string, int>
*/
private function free_features(): array {
return [
'analytics_days' => 7,
'cache_limit' => 100,
'auto_meta_daily' => 10,
];
}
}