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; + } +}