feat(share): WeChat JS-SDK signature + footer share config

Port zibll WeChatShare: ticket/token cache, page image resolution,
jweixin enqueue, and WeChatShareDate for in-app timeline/message share.
This commit is contained in:
2026-07-19 01:34:40 +08:00
parent 5f398dd9d0
commit 179474da3e
2 changed files with 263 additions and 0 deletions
+260
View File
@@ -0,0 +1,260 @@
<?php
/**
* WeChat JS-SDK share signature (zibll ZibWeChatShare parity).
*
* @package AKSRT
*/
namespace AKSRT\Frontend;
defined('ABSPATH') || exit;
class WeChatShare {
private static $instance = null;
/** @var array{wechat_enabled:bool,wechat_appid:string,wechat_app_secret:string,only_logo:bool} */
private $config = [];
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
$this->config = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_share_settings()
: [];
if (empty($this->config['wechat_enabled'])) {
return;
}
if (empty($this->config['wechat_appid']) || empty($this->config['wechat_app_secret'])) {
return;
}
add_action('wp_enqueue_scripts', [$this, 'enqueue_sdk'], 20);
add_action('wp_footer', [$this, 'print_share_date'], 99);
}
public function enqueue_sdk(): void {
// Only needed in WeChat browser, but enqueue lightly for all (script is external CDN).
wp_enqueue_script(
'aksrt-jweixin',
'https://res.wx.qq.com/open/js/jweixin-1.6.0.js',
[],
'1.6.0',
true
);
// Ensure our bridge runs after jweixin.
if (wp_script_is('aksrt-weixin-share', 'registered') || wp_script_is('aksrt-weixin-share', 'enqueued')) {
wp_script_add_data('aksrt-weixin-share', 'group', 1);
}
}
public function print_share_date(): void {
$url = $this->current_url();
$sign = $this->get_sign($url);
if (!$sign) {
return;
}
$img = $this->resolve_image();
$title = wp_get_document_title();
$desc = '';
if (is_singular()) {
$post = get_post();
if ($post) {
$desc = wp_strip_all_tags(get_the_excerpt($post));
}
}
if ($desc === '') {
$desc = (string) get_bloginfo('description');
}
$payload = [
'appId' => $sign['appId'],
'timestamp' => (string) $sign['timestamp'],
'nonceStr' => $sign['nonceStr'],
'signature' => $sign['signature'],
'url' => $url,
'title' => $title,
'img' => $img,
'desc' => $desc,
];
?>
<script type="text/javascript">
window.WeChatShareDate = <?php echo wp_json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
(function () {
function applyConfig() {
if (!window.wx || typeof window.wx.config !== 'function' || !window.WeChatShareDate) return;
var e = window.WeChatShareDate;
try {
window.wx.config({
debug: false,
appId: e.appId,
timestamp: e.timestamp,
nonceStr: e.nonceStr,
signature: e.signature,
jsApiList: ['updateTimelineShareData', 'updateAppMessageShareData']
});
window.wx.ready(function () {
var title = e.title || document.title;
var link = e.url || window.location.href;
var desc = e.desc || ((document.querySelector('meta[name="description"]') || {}).content || '');
var img = e.img || '';
if (typeof window.wx.updateTimelineShareData === 'function') {
window.wx.updateTimelineShareData({ title: title, link: link, imgUrl: img });
}
if (typeof window.wx.updateAppMessageShareData === 'function') {
window.wx.updateAppMessageShareData({ title: title, desc: desc, link: link, imgUrl: img });
}
});
} catch (err) {}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { setTimeout(applyConfig, 50); });
} else {
setTimeout(applyConfig, 50);
}
})();
</script>
<?php
}
/**
* @return array{appId:string,nonceStr:string,timestamp:int,signature:string}|null
*/
public function get_sign(string $url): ?array {
$ticket = $this->get_jsapi_ticket();
if ($ticket === '') {
return null;
}
$timestamp = time();
$nonce = $this->nonce_string();
$raw = "jsapi_ticket={$ticket}&noncestr={$nonce}&timestamp={$timestamp}&url={$url}";
return [
'appId' => (string) $this->config['wechat_appid'],
'nonceStr' => $nonce,
'timestamp' => $timestamp,
'signature' => sha1($raw),
];
}
private function current_url(): string {
$https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443)
|| (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
$protocol = $https ? 'https://' : 'http://';
$host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash((string) $_SERVER['HTTP_HOST'])) : wp_parse_url(home_url('/'), PHP_URL_HOST);
$uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/';
// Drop hash fragment if any (should not be present in REQUEST_URI).
return $protocol . $host . $uri;
}
private function resolve_image(): string {
$only_logo = !empty($this->config['only_logo']);
$default = '';
if (!empty($this->config['share_img'])) {
$default = (string) $this->config['share_img'];
} elseif (class_exists('\AKSRT\Settings\Settings')) {
$default = (string) (\AKSRT\Settings\Settings::get_value('favicon', '') ?: \AKSRT\Settings\Settings::get_value('iconpng', ''));
}
if ($only_logo) {
return $default;
}
$pic = '';
if (is_singular()) {
$post_id = get_queried_object_id();
if ($post_id && has_post_thumbnail($post_id)) {
$pic = (string) get_the_post_thumbnail_url($post_id, 'full');
}
} elseif (is_author()) {
$author = get_queried_object();
if ($author && !empty($author->ID)) {
$pic = (string) get_user_meta((int) $author->ID, 'custom_avatar', true);
if ($pic === '') {
$pic = get_avatar_url((int) $author->ID, ['size' => 300]);
}
}
} elseif (is_category() || is_tag() || is_tax()) {
$term = get_queried_object();
if ($term && !empty($term->term_id)) {
$img_id = (int) get_term_meta((int) $term->term_id, 'thumbnail_id', true);
if ($img_id) {
$pic = (string) (wp_get_attachment_image_url($img_id, 'full') ?: '');
}
}
}
return $pic !== '' ? $pic : $default;
}
private function get_jsapi_ticket(): string {
$opt = get_option('aksrt_wechatshare_ticket', []);
$now = time() + 300;
if (is_array($opt) && !empty($opt['ticket']) && !empty($opt['expiration_time']) && (int) $opt['expiration_time'] > $now) {
return (string) $opt['ticket'];
}
$token = $this->get_access_token();
if ($token === '') {
return '';
}
$url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=' . rawurlencode($token) . '&type=jsapi';
$res = $this->http_json($url);
if (empty($res['ticket'])) {
return '';
}
$res['expiration_time'] = time() + max(60, (int) ($res['expires_in'] ?? 7200));
update_option('aksrt_wechatshare_ticket', $res, false);
return (string) $res['ticket'];
}
private function get_access_token(): string {
// Prefer shared GZH token cache when present.
$opt = get_option('aksrt_weixingzh_access_token', []);
$now = time() + 300;
if (is_array($opt) && !empty($opt['access_token']) && !empty($opt['expiration_time']) && (int) $opt['expiration_time'] > $now) {
return (string) $opt['access_token'];
}
$appid = (string) $this->config['wechat_appid'];
$secret = (string) $this->config['wechat_app_secret'];
$url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' . rawurlencode($appid) . '&secret=' . rawurlencode($secret);
$res = $this->http_json($url);
if (empty($res['access_token'])) {
return '';
}
$res['expiration_time'] = time() + max(60, (int) ($res['expires_in'] ?? 7200));
update_option('aksrt_weixingzh_access_token', $res, false);
return (string) $res['access_token'];
}
/**
* @return array<string,mixed>
*/
private function http_json(string $url): array {
$response = wp_remote_get($url, [
'timeout' => 8,
'headers' => ['Accept' => 'application/json'],
]);
if (is_wp_error($response)) {
return [];
}
$body = (string) wp_remote_retrieve_body($response);
$data = json_decode($body, true);
return is_array($data) ? $data : [];
}
private function nonce_string(int $length = 16): string {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$str = '';
$max = strlen($chars) - 1;
for ($i = 0; $i < $length; $i++) {
$str .= $chars[random_int(0, $max)];
}
return $str;
}
}