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:
@@ -140,6 +140,9 @@ if (get_option('aksrt_flushed_version') !== AKSRT_VERSION) {
|
||||
if (class_exists('\AKSRT\Auth\WeixinGzh')) {
|
||||
\AKSRT\Auth\WeixinGzh::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Frontend\WeChatShare')) {
|
||||
\AKSRT\Frontend\WeChatShare::get_instance()->init();
|
||||
}
|
||||
|
||||
// 初始化代理登录(根据配置模式选择性加载)
|
||||
$aksrt_proxy_mode = \AKSRT\Settings\Settings::oauth_proxy_mode();
|
||||
|
||||
@@ -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}×tamp={$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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user