Auth.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <?php
  2. /**
  3. * This file is part of the ForkBB <https://github.com/forkbb>.
  4. *
  5. * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
  6. * @license The MIT License (MIT)
  7. */
  8. declare(strict_types=1);
  9. namespace ForkBB\Models\Pages;
  10. use ForkBB\Core\Validator;
  11. use ForkBB\Core\Exceptions\MailException;
  12. use ForkBB\Models\Page;
  13. use ForkBB\Models\User\User;
  14. use SensitiveParameter;
  15. use function \ForkBB\__;
  16. class Auth extends Page
  17. {
  18. /**
  19. * Выход пользователя
  20. */
  21. public function logout(array $args): Page
  22. {
  23. if (! $this->c->Csrf->verify($args['token'], 'Logout', $args)) {
  24. $this->c->Log->warning('Logout: fail', [
  25. 'user' => $this->user->fLog(),
  26. ]);
  27. return $this->c->Redirect->page('Index')->message($this->c->Csrf->getError());
  28. }
  29. $this->c->Cookie->deleteUser();
  30. $this->c->Online->delete($this->user);
  31. $this->c->users->updateLastVisit($this->user);
  32. $this->c->Log->info('Logout: ok', [
  33. 'user' => $this->user->fLog(),
  34. ]);
  35. $this->c->Lang->load('auth');
  36. return $this->c->Redirect->page('Index')->message('Logout redirect');
  37. }
  38. /**
  39. * Вход на форум
  40. */
  41. public function login(array $args, string $method, string $username = ''): Page
  42. {
  43. $this->c->Lang->load('validator');
  44. $this->c->Lang->load('auth');
  45. $v = null;
  46. if ('POST' === $method) {
  47. $v = $this->c->Validator->reset()
  48. ->addValidators([
  49. 'login_check' => [$this, 'vLoginCheck'],
  50. ])->addRules([
  51. 'token' => 'token:Login',
  52. 'redirect' => 'required|referer:Index',
  53. 'username' => 'required|string',
  54. 'password' => 'required|string|max:100000|login_check',
  55. 'save' => 'checkbox',
  56. 'login' => 'required|string',
  57. ])->addAliases([
  58. 'username' => 'Username',
  59. 'password' => 'Passphrase',
  60. ]);
  61. $v = $this->c->Test->beforeValidation($v);
  62. if ($v->validation($_POST, true)) {
  63. $this->loginEnd($v);
  64. return $this->c->Redirect->url($v->redirect)->message('Login redirect');
  65. }
  66. $this->fIswev = $v->getErrors();
  67. $this->c->Log->warning('Login: fail', [
  68. 'user' => $this->user->fLog(),
  69. 'errors' => $v->getErrorsWithoutType(),
  70. 'form' => $v->getData(false, ['token', 'password']),
  71. ]);
  72. $this->httpStatus = 400;
  73. }
  74. $ref = $this->c->Secury->replInvalidChars($_SERVER['HTTP_REFERER'] ?? '');
  75. $this->hhsLevel = 'secure';
  76. $this->fIndex = self::FI_LOGIN;
  77. $this->nameTpl = 'login';
  78. $this->onlinePos = 'login';
  79. $this->onlineDetail = null;
  80. $this->robots = 'noindex';
  81. $this->titles = 'Login';
  82. $this->regLink = 1 === $this->c->config->b_regs_allow ? $this->c->Router->link('Register') : null;
  83. $username = $v->username ?? $username;
  84. $save = $v->save ?? 1;
  85. $redirect = $v->redirect ?? $this->c->Router->validate($ref, 'Index');
  86. $this->form = $this->formLogin($username, $save, $redirect);
  87. return $this;
  88. }
  89. /**
  90. * Обрабатывает вход пользователя
  91. */
  92. protected function loginEnd(Validator $v): void
  93. {
  94. $this->c->users->updateLoginIpCache($this->userAfterLogin, true); // ????
  95. // сбросить запрос на смену кодовой фразы
  96. if (32 === \strlen($this->userAfterLogin->activate_string)) {
  97. $this->userAfterLogin->activate_string = '';
  98. }
  99. // изменения юзера в базе
  100. $this->c->users->update($this->userAfterLogin);
  101. $this->c->Online->delete($this->user);
  102. $this->c->Cookie->setUser($this->userAfterLogin, (bool) $v->save);
  103. $this->c->Log->info('Login: ok', [
  104. 'user' => $this->userAfterLogin->fLog(),
  105. 'form' => $v->getData(false, ['token', 'password']),
  106. 'headers' => true,
  107. ]);
  108. }
  109. /**
  110. * Подготавливает массив данных для формы
  111. */
  112. protected function formLogin(string $username, /* mixed */ $save, string $redirect): array
  113. {
  114. return [
  115. 'action' => $this->c->Router->link('Login'),
  116. 'hidden' => [
  117. 'token' => $this->c->Csrf->create('Login'),
  118. 'redirect' => $redirect,
  119. ],
  120. 'sets' => [
  121. 'login' => [
  122. 'fields' => [
  123. 'username' => [
  124. 'autofocus' => true,
  125. 'type' => 'text',
  126. 'value' => $username,
  127. 'caption' => 'Username',
  128. 'required' => true,
  129. ],
  130. 'password' => [
  131. 'id' => 'passinlogin',
  132. 'type' => 'password',
  133. 'caption' => 'Passphrase',
  134. 'help' => ['<a href="%s">Forgotten?</a>', $this->c->Router->link('Forget')],
  135. 'required' => true,
  136. ],
  137. 'save' => [
  138. 'type' => 'checkbox',
  139. 'label' => 'Remember me',
  140. 'checked' => $save,
  141. ],
  142. ],
  143. ],
  144. ],
  145. 'btns' => [
  146. 'login' => [
  147. 'type' => 'submit',
  148. 'value' => __('Sign in'),
  149. ],
  150. ],
  151. ];
  152. }
  153. /**
  154. * Проверка пользователя по базе
  155. */
  156. public function vLoginCheck(
  157. Validator $v,
  158. #[SensitiveParameter]
  159. string $password
  160. ): string {
  161. if (empty($v->getErrors())) {
  162. $this->userAfterLogin = $this->c->users->loadByName($v->username);
  163. if (
  164. ! $this->userAfterLogin instanceof User
  165. || $this->userAfterLogin->isGuest
  166. ) {
  167. $v->addError('Wrong user/pass');
  168. } elseif ($this->userAfterLogin->isUnverified) {
  169. $v->addError('Account is not activated', 'w');
  170. } elseif (! \password_verify($password, $this->userAfterLogin->password)) {
  171. $v->addError('Wrong user/pass');
  172. }
  173. }
  174. if (! empty($v->getErrors())) {
  175. $this->userAfterLogin = null;
  176. }
  177. return $password;
  178. }
  179. /**
  180. * Запрос на смену кодовой фразы
  181. */
  182. public function forget(array $args, string $method, string $email = ''): Page
  183. {
  184. $this->c->Lang->load('validator');
  185. $this->c->Lang->load('auth');
  186. $v = null;
  187. if ('POST' === $method) {
  188. $v = $this->c->Validator->reset()
  189. ->addValidators([
  190. ])->addRules([
  191. 'token' => 'token:Forget',
  192. 'email' => 'required|string:trim|email',
  193. 'submit' => 'required|string',
  194. ])->addAliases([
  195. ])->addMessages([
  196. 'email.email' => 'Invalid email',
  197. ])->addArguments([
  198. ]);
  199. $v = $this->c->Test->beforeValidation($v);
  200. $isValid = $v->validation($_POST, true);
  201. $context = [
  202. 'user' => $this->user->fLog(), // ???? Guest only?
  203. 'errors' => $v->getErrorsWithoutType(),
  204. 'form' => $v->getData(false, ['token']),
  205. 'headers' => true,
  206. ];
  207. if ($isValid) {
  208. $tmpUser = $this->c->users->create();
  209. $isSent = false;
  210. $v = $v->reset()
  211. ->addRules([
  212. 'email' => 'required|string:trim|email:nosoloban,exists,flood',
  213. ])->addArguments([
  214. 'email.email' => $tmpUser, // сюда идет возрат данных по найденному пользователю
  215. ]);
  216. if (
  217. $v->validation($_POST)
  218. && 0 === $this->c->bans->banFromName($tmpUser->username)
  219. ) {
  220. $this->c->Csrf->setHashExpiration(259200); // ???? хэш действует 72 часа
  221. $key = $this->c->Secury->randomPass(32);
  222. $link = $this->c->Router->link(
  223. 'ChangePassword',
  224. [
  225. 'id' => $tmpUser->id,
  226. 'key' => $key,
  227. ]
  228. );
  229. $tplData = [
  230. 'fRootLink' => $this->c->Router->link('Index'),
  231. 'fMailer' => __(['Mailer', $this->c->config->o_board_title]),
  232. 'username' => $tmpUser->username,
  233. 'link' => $link,
  234. ];
  235. try {
  236. $isSent = $this->c->Mail
  237. ->reset()
  238. ->setMaxRecipients(1)
  239. ->setFolder($this->c->DIR_LANG)
  240. ->setLanguage($tmpUser->language)
  241. ->setTo($tmpUser->email, $tmpUser->username)
  242. ->setFrom($this->c->config->o_webmaster_email, $tplData['fMailer'])
  243. ->setTpl('passphrase_reset.tpl', $tplData)
  244. ->send();
  245. } catch (MailException $e) {
  246. $this->c->Log->error('Passphrase reset: email form, MailException', [
  247. 'exception' => $e,
  248. 'headers' => false,
  249. ]);
  250. }
  251. if ($isSent) {
  252. $tmpUser->activate_string = $key;
  253. $tmpUser->last_email_sent = \time();
  254. $this->c->users->update($tmpUser);
  255. $this->c->Log->info('Passphrase reset: email form, ok', $context);
  256. }
  257. }
  258. if (! $isSent) {
  259. $context['errors'] = $v->getErrorsWithoutType();
  260. $this->c->Log->warning('Passphrase reset: email form, fail', $context);
  261. }
  262. return $this->c->Message->message(['Forget mail', $this->c->config->o_admin_email], false, 0);
  263. }
  264. $this->fIswev = $v->getErrors();
  265. $this->c->Log->warning('Passphrase reset: email form, fail', $context);
  266. $this->httpStatus = 400;
  267. }
  268. $this->hhsLevel = 'secure';
  269. $this->fIndex = self::FI_LOGIN;
  270. $this->nameTpl = 'passphrase_reset';
  271. $this->onlinePos = 'passphrase_reset';
  272. $this->onlineDetail = null;
  273. $this->robots = 'noindex';
  274. $this->titles = 'Passphrase reset';
  275. $this->form = $this->formForget($v->email ?? $email);
  276. return $this;
  277. }
  278. /**
  279. * Подготавливает массив данных для формы
  280. */
  281. protected function formForget(string $email): array
  282. {
  283. return [
  284. 'action' => $this->c->Router->link('Forget'),
  285. 'hidden' => [
  286. 'token' => $this->c->Csrf->create('Forget'),
  287. ],
  288. 'sets' => [
  289. 'forget' => [
  290. 'fields' => [
  291. 'email' => [
  292. 'autofocus' => true,
  293. 'type' => 'text',
  294. 'maxlength' => '80',
  295. 'value' => $email,
  296. 'caption' => 'Email',
  297. 'help' => 'Passphrase reset info',
  298. 'required' => true,
  299. 'pattern' => '.+@.+',
  300. ],
  301. ],
  302. ],
  303. ],
  304. 'btns' => [
  305. 'submit' => [
  306. 'type' => 'submit',
  307. 'value' => __('Send email'),
  308. ],
  309. ],
  310. ];
  311. }
  312. /**
  313. * Смена кодовой фразы
  314. */
  315. public function changePass(array $args, string $method): Page
  316. {
  317. if (
  318. ! $this->c->Csrf->verify($args['hash'], 'ChangePassword', $args)
  319. || ! ($user = $this->c->users->load($args['id'])) instanceof User
  320. || ! \hash_equals($user->activate_string, $args['key'])
  321. ) {
  322. $this->c->Log->warning('Passphrase reset: confirmation, fail', [
  323. 'user' => $user instanceof User ? $user->fLog() : $this->user->fLog(),
  324. 'args' => $args,
  325. ]);
  326. // что-то пошло не так
  327. return $this->c->Message->message('Bad request', false);
  328. }
  329. $this->c->Lang->load('validator');
  330. $this->c->Lang->load('auth');
  331. if ('POST' === $method) {
  332. $v = $this->c->Validator->reset()
  333. ->addRules([
  334. 'token' => 'token:ChangePassword',
  335. 'password' => 'required|string|min:16|max:100000|password',
  336. 'password2' => 'required|same:password',
  337. 'submit' => 'required|string',
  338. ])->addAliases([
  339. 'password' => 'New pass',
  340. 'password2' => 'Confirm new pass',
  341. ])->addArguments([
  342. 'token' => $args,
  343. ])->addMessages([
  344. 'password.password' => 'Pass format',
  345. 'password2.same' => 'Pass not match',
  346. ]);
  347. $v = $this->c->Test->beforeValidation($v);
  348. if ($v->validation($_POST, true)) {
  349. $user->password = \password_hash($v->password, \PASSWORD_DEFAULT);
  350. $user->email_confirmed = 1;
  351. $user->activate_string = '';
  352. $this->c->users->update($user);
  353. $this->fIswev = ['s', 'Pass updated'];
  354. $this->c->Log->info('Passphrase reset: ok', [
  355. 'user' => $user->fLog(),
  356. ]);
  357. return $this->login([], 'GET');
  358. }
  359. $this->fIswev = $v->getErrors();
  360. $this->c->Log->warning('Passphrase reset: change form, fail', [
  361. 'user' => $user->fLog(),
  362. 'errors' => $v->getErrorsWithoutType(),
  363. 'form' => $v->getData(false, ['token', 'password', 'password2']),
  364. ]);
  365. $this->httpStatus = 400;
  366. }
  367. // активация аккаунта (письмо активации не дошло, заказали восстановление)
  368. if ($user->isUnverified) {
  369. $user->group_id = $this->c->config->i_default_user_group;
  370. $user->email_confirmed = 1;
  371. $this->c->users->update($user);
  372. $this->fIswev = ['i', 'Account activated'];
  373. $this->c->Log->info('Account activation: ok', [
  374. 'user' => $user->fLog(),
  375. ]);
  376. }
  377. $this->hhsLevel = 'secure';
  378. $this->fIndex = self::FI_LOGIN;
  379. $this->nameTpl = 'change_passphrase';
  380. $this->onlinePos = 'change_passphrase';
  381. $this->onlineDetail = null;
  382. $this->robots = 'noindex';
  383. $this->titles = 'Passphrase reset';
  384. $this->form = $this->formChange($args);
  385. return $this;
  386. }
  387. /**
  388. * Подготавливает массив данных для формы
  389. */
  390. protected function formChange(array $args): array
  391. {
  392. return [
  393. 'action' => $this->c->Router->link('ChangePassword', $args),
  394. 'hidden' => [
  395. 'token' => $this->c->Csrf->create('ChangePassword', $args),
  396. ],
  397. 'sets' => [
  398. 'forget' => [
  399. 'fields' => [
  400. 'password' => [
  401. 'autofocus' => true,
  402. 'type' => 'password',
  403. 'caption' => 'New pass',
  404. 'required' => true,
  405. 'pattern' => '^.{16,}$',
  406. ],
  407. 'password2' => [
  408. 'type' => 'password',
  409. 'caption' => 'Confirm new pass',
  410. 'help' => 'Passphrase help',
  411. 'required' => true,
  412. 'pattern' => '^.{16,}$',
  413. ],
  414. ],
  415. ],
  416. ],
  417. 'btns' => [
  418. 'submit' => [
  419. 'type' => 'submit',
  420. 'value' => __('Change passphrase'),
  421. ],
  422. ],
  423. ];
  424. }
  425. }