diff --git a/functions.php b/functions.php
index 5767db5d..62e1beaa 100644
--- a/functions.php
+++ b/functions.php
@@ -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();
diff --git a/src/AKSRT/Frontend/WeChatShare.php b/src/AKSRT/Frontend/WeChatShare.php
new file mode 100644
index 00000000..07836449
--- /dev/null
+++ b/src/AKSRT/Frontend/WeChatShare.php
@@ -0,0 +1,260 @@
+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,
+ ];
+ ?>
+
+ 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
+ */
+ 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;
+ }
+}