better-staridc-MNBT
1<?php
2/**
3 * user_info 插件 - 认证函数库
4 *
5 * 独立于核心 member.php 的用户认证:
6 * - cookie 名:account_token(与核心 user_token 不冲突)
7 * - 加密方式:authcode($user_id \t $session_hash, SYS_KEY)
8 * - session_hash = md5($user_id . $password_hash . SYS_KEY)
9 * 修改密码后 session_hash 变化,旧 cookie 自动失效
10 * - 密码哈希:password_hash / password_verify(bcrypt)
11 */
12
13if (!defined('IN_CRONLITE')) {
14 exit;
15}
16
17/**
18 * 生成带站点 base path 前缀的 URL(用于页面链接)。
19 * 子目录部署时自动补全前缀。
20 */
21function user_info_url($path = '')
22{
23 $scriptName = isset($_SERVER['SCRIPT_NAME']) ? str_replace('\\', '/', $_SERVER['SCRIPT_NAME']) : '';
24 $basePath = rtrim(str_replace('\\', '/', dirname($scriptName)), '/');
25 if ($basePath === '.' || $basePath === '/') {
26 $basePath = '';
27 }
28 // 使用查询参数路由(index.php?_r=/path),避免依赖 Web 服务器 rewrite
29 $p = ltrim($path, '/');
30 $qpos = strpos($p, '?');
31 if ($qpos !== false) {
32 $route = substr($p, 0, $qpos);
33 $query = substr($p, $qpos + 1);
34 return $basePath . '/index.php?_r=/' . $route . '&' . $query;
35 }
36 return $basePath . '/index.php?_r=/' . $p;
37}
38
39/**
40 * 插件静态资源 URL。
41 */
42function user_info_asset_url($path = '')
43{
44 return mnbt_plugin_url('user_info', 'assets/' . ltrim($path, '/'));
45}
46
47/**
48 * 设置登录 cookie。
49 */
50function user_info_auth_login($user_id, $password_hash)
51{
52 $session_hash = md5($user_id . $password_hash . SYS_KEY);
53 $token = authcode($user_id . "\t" . $session_hash, 'ENCODE', SYS_KEY);
54 mnbt_rotate_login_session();
55 mnbt_set_auth_cookie('account_token', $token, time() + 604800);
56}
57
58/**
59 * 清除登录 cookie。
60 */
61function user_info_auth_logout()
62{
63 mnbt_set_auth_cookie('account_token', '', time() - 604800);
64}
65
66/**
67 * 获取当前登录用户(数组),未登录返回 null。
68 */
69function user_info_auth_current()
70{
71 global $DB;
72 if (empty($_COOKIE['account_token'])) {
73 return null;
74 }
75 $token = daddslashes($_COOKIE['account_token']);
76 $decoded = authcode($token, 'DECODE', SYS_KEY);
77 if ($decoded === '' || $decoded === false || $decoded === null) {
78 return null;
79 }
80 $parts = explode("\t", $decoded);
81 if (count($parts) !== 2) {
82 return null;
83 }
84 $user_id = (int)$parts[0];
85 $session_hash = $parts[1];
86 if ($user_id <= 0 || $session_hash === '') {
87 return null;
88 }
89 $user = $DB->get_row_prepare("SELECT * FROM MN_plugin_user WHERE id=? LIMIT 1", [$user_id]);
90 if (!$user) {
91 return null;
92 }
93 if ((int)$user['status'] !== 1) {
94 return null;
95 }
96 $expected = md5($user['id'] . $user['password_hash'] . SYS_KEY);
97 if ($session_hash !== $expected) {
98 return null;
99 }
100 return $user;
101}
102
103/**
104 * 要求登录,未登录跳转登录页。返回用户数组。
105 */
106function user_info_auth_require()
107{
108 $user = user_info_auth_current();
109 if (!$user) {
110 header('Location: ' . user_info_url('account/login'));
111 exit;
112 }
113 return $user;
114}
115
116/**
117 * 解析主题提供的 account 入口文件(如 tdesign 主题的 SPA 入口)。
118 * 当前用户主题下存在 templates/{theme}/account/{view}.php 时返回路径,否则返回 null。
119 *
120 * @param string $view views 名称:login/register/profile/password/dashboard
121 * @return string|null
122 */
123function user_info_theme_entry($view)
124{
125 if (!function_exists('mnbt_theme_name') || !defined('MNBT_THEME_ROOT')) {
126 return null;
127 }
128 $map = [
129 'login' => 'login.php',
130 'register' => 'register.php',
131 'profile' => 'profile.php',
132 'password' => 'password.php',
133 'dashboard' => 'index.php',
134 ];
135 if (!isset($map[$view])) {
136 return null;
137 }
138 $theme = mnbt_theme_name('user');
139 $file = MNBT_THEME_ROOT . $theme . '/account/' . $map[$view];
140 return is_file($file) ? $file : null;
141}
142
143/**
144 * 渲染视图文件(带布局)。
145 *
146 * 当前用户主题若提供 account 入口(如 tdesign SPA),优先交给主题渲染;
147 * 否则回退到插件自带 Layui 布局。
148 *
149 * @param string $view views 目录下的文件名(不含 .php)
150 * @param array $vars 传给视图的变量
151 */
152function user_info_render($view, $vars = [])
153{
154 $vars['current_user'] = user_info_auth_current();
155 $vars['asset_url'] = user_info_asset_url();
156 $vars['url'] = 'user_info_url';
157 extract($vars, EXTR_SKIP);
158
159 $themeEntry = user_info_theme_entry($view);
160 if ($themeEntry !== null) {
161 // 主题入口在函数作用域内 include,需要展开全局变量($conf/$DB/$date 等)
162 extract($GLOBALS, EXTR_SKIP);
163 include $themeEntry;
164 return;
165 }
166
167 $viewFile = mnbt_plugin_path('user_info') . 'views/' . $view . '.php';
168 if (!is_file($viewFile)) {
169 http_response_code(500);
170 echo 'View not found: ' . htmlspecialchars($view);
171 return;
172 }
173 include $viewFile;
174}
175
176/**
177 * 输出 JSON 并退出。
178 */
179function user_info_json($code, $extra = [])
180{
181 @header('Content-Type: application/json; charset=UTF-8');
182 $payload = ['code' => $code];
183 if (is_array($extra)) {
184 $payload = array_merge($payload, $extra);
185 }
186 echo json_encode($payload, JSON_UNESCAPED_UNICODE);
187 exit;
188}