wenpai-updater/class-wenpai-updater.php
feibisi c4ae854861
Some checks are pending
gitleaks 密钥泄露扫描 / gitleaks (push) Waiting to run
feicode/ai-security No obvious risky pattern in latest diff
WordPress 插件 CI / ci (push) Successful in -8h1m15s
fix: phpcbf 自动修复 + phpcs.xml 代码规范配置
- phpcbf 自动修复 204 个格式问题(缩进、空格、换行等)
- 添加 phpcs.xml 排除 WordPress hook 回调中未使用参数的误报
- CI 现在应该通过

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-18 15:16:51 +08:00

243 lines
6.1 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
/**
* WenPai 插件更新器
*
* 为文派系插件提供自建更新服务支持。
* 通过文派云桥 (WenPai Bridge) 检查插件更新,
* 利用 WordPress 5.8+ 的 Update URI 机制。
*
* 当 wp-china-yes 插件激活并启用集中更新时,
* 此更新器会通过 wenpai_updater_override filter 自动让位。
*
* @package WenPai
* @version 1.0.0
* @requires WordPress 5.8+
* @requires PHP 7.4+
* @link https://feicode.com/WenPai-org
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( class_exists( 'WenPai_Updater' ) ) {
return;
}
class WenPai_Updater {
/**
* 更新器版本号。
*
* @var string
*/
const VERSION = '1.0.0';
/**
* 云桥 API 地址。
*
* @var string
*/
const API_URL = 'https://updates.wenpai.net/api/v1';
/**
* 插件主文件 basename如 wpslug/wpslug.php
*
* @var string
*/
private $plugin_file;
/**
* 插件 slug如 wpslug
*
* @var string
*/
private $slug;
/**
* 当前插件版本。
*
* @var string
*/
private $version;
/**
* 初始化更新器。
*
* @param string $plugin_file 插件主文件路径plugin_basename 格式)。
* @param string $version 当前插件版本号。
*/
public function __construct( string $plugin_file, string $version ) {
$this->plugin_file = $plugin_file;
$this->slug = dirname( $plugin_file );
$this->version = $version;
// 检查是否被 wp-china-yes 集中更新接管
$is_overridden = apply_filters(
'wenpai_updater_override',
false,
$this->slug
);
if ( ! $is_overridden ) {
$this->register_hooks();
}
}
/**
* 注册 WordPress hooks。
*/
private function register_hooks(): void {
// Update URI: https://updates.wenpai.net 触发此 filter
add_filter(
'update_plugins_updates.wenpai.net',
array( $this, 'check_update' ),
10,
4
);
// 插件详情弹窗
add_filter( 'plugins_api', array( $this, 'plugin_info' ), 20, 3 );
}
/**
* 检查插件更新。
*
* WordPress 在检查更新时,对声明了 Update URI 的插件
* 触发 update_plugins_{hostname} filter。
*
* @param array|false $update 当前更新数据。
* @param array $plugin_data 插件头信息。
* @param string $plugin_file 插件文件路径。
* @param string[] $locales 语言列表。
* @return object|false 更新数据或 false。
*/
public function check_update( $update, array $plugin_data, string $plugin_file, array $locales ) {
if ( $plugin_file !== $this->plugin_file ) {
return $update;
}
$response = $this->api_request(
'update-check',
array(
'plugins' => array(
$this->plugin_file => array(
'Version' => $this->version,
),
),
)
);
if ( is_wp_error( $response ) || empty( $response['plugins'][ $this->plugin_file ] ) ) {
return $update;
}
$data = $response['plugins'][ $this->plugin_file ];
return (object) array(
'id' => $data['id'] ?? '',
'slug' => $data['slug'] ?? $this->slug,
'plugin' => $this->plugin_file,
'version' => $data['version'] ?? '',
'new_version' => $data['version'] ?? '',
'url' => $data['url'] ?? '',
'package' => $data['package'] ?? '',
'icons' => $data['icons'] ?? array(),
'banners' => $data['banners'] ?? array(),
'requires' => $data['requires'] ?? '',
'tested' => $data['tested'] ?? '',
'requires_php' => $data['requires_php'] ?? '',
);
}
/**
* 插件详情弹窗数据。
*
* 当用户在 WP 后台点击"查看详情"时触发。
*
* @param false|object|array $result 当前结果。
* @param string $action API 动作。
* @param object $args 请求参数。
* @return false|object 插件信息或 false。
*/
public function plugin_info( $result, string $action, object $args ) {
if ( 'plugin_information' !== $action ) {
return $result;
}
if ( ! isset( $args->slug ) || $args->slug !== $this->slug ) {
return $result;
}
$response = $this->api_request( "plugins/{$this->slug}/info" );
if ( is_wp_error( $response ) ) {
return $result;
}
$info = new stdClass();
$info->name = $response['name'] ?? '';
$info->slug = $response['slug'] ?? $this->slug;
$info->version = $response['version'] ?? '';
$info->author = $response['author'] ?? '';
$info->homepage = $response['homepage'] ?? '';
$info->download_link = $response['download_link'] ?? '';
$info->requires = $response['requires'] ?? '';
$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();
return $info;
}
/**
* 向云桥 API 发送请求。
*
* @param string $endpoint API 端点(不含 /api/v1/ 前缀)。
* @param array|null $body POST 请求体null 则用 GET
* @return array|WP_Error 解码后的响应或错误。
*/
private function api_request( string $endpoint, ?array $body = null ) {
$url = self::API_URL . '/' . ltrim( $endpoint, '/' );
$args = array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json',
),
);
if ( null !== $body ) {
$args['headers']['Content-Type'] = 'application/json';
$args['body'] = wp_json_encode( $body );
$response = wp_remote_post( $url, $args );
} else {
$response = wp_remote_get( $url, $args );
}
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $code ) {
return new WP_Error(
'wenpai_bridge_error',
sprintf( 'WenPai Bridge API returned %d', $code )
);
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $data ) ) {
return new WP_Error(
'wenpai_bridge_parse_error',
'Invalid JSON response from WenPai Bridge'
);
}
return $data;
}
}