PageRenderTime 51ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/php/user.php

https://gitlab.com/dobrado/dobrado
PHP | 420 lines | 351 code | 23 blank | 46 comment | 88 complexity | dbfcec451428a2c3b2deb17fdf00954e MD5 | raw file
Possible License(s): AGPL-3.0, LGPL-2.1
  1. <?php
  2. // Dobrado Content Management System
  3. // Copyright (C) 2019 Malcolm Blaney
  4. //
  5. // This program is free software: you can redistribute it and/or modify
  6. // it under the terms of the GNU Affero General Public License as
  7. // published by the Free Software Foundation, either version 3 of the
  8. // License, or (at your option) any later version.
  9. //
  10. // This program is distributed in the hope that it will be useful,
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. // GNU Affero General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Affero General Public License
  16. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. class User {
  18. public $name = '';
  19. public $group = '';
  20. public $page = '';
  21. public $config = NULL;
  22. public $settings = [];
  23. public $active = true;
  24. public $loggedIn = false;
  25. public $loginFailed = false;
  26. public $loginStatus = '';
  27. public $emailReset = false;
  28. public $defaultPage = false;
  29. public $canEditSite = false;
  30. public $canEditPage = false;
  31. public $canCopyPage = false;
  32. public $canViewPage = false;
  33. public function __construct($user = '', $group = '', $timezone = '') {
  34. $this->config = new Config();
  35. if ($user !== '') {
  36. $this->name = $user;
  37. if ($group === '') {
  38. $this->SetGroup();
  39. }
  40. else {
  41. $this->group = $group;
  42. }
  43. $this->Settings();
  44. $this->config->SetUser($user, $timezone);
  45. return;
  46. }
  47. $from_admin = false;
  48. // Check if a (possibly different) user is trying to log in.
  49. if (isset($_POST['user'])) {
  50. $single_user = substitute('account-single-user') === 'true';
  51. $mysqli = connect_db();
  52. $this->name = $mysqli->escape_string(strtolower(trim($_POST['user'])));
  53. if ($single_user) {
  54. $query = 'SELECT value FROM settings WHERE user = "admin" AND ' .
  55. 'label = "account" AND name = "username"';
  56. if ($mysqli_result = $mysqli->query($query)) {
  57. if ($settings = $mysqli_result->fetch_assoc()) {
  58. if ($this->name === $settings['value']) $this->name = 'admin';
  59. }
  60. }
  61. else {
  62. log_db('User: ' . $mysqli->error);
  63. }
  64. }
  65. $mysqli->close();
  66. if (!$single_user &&
  67. $this->name === 'guest' && $this->config->GuestAllowed()) {
  68. $this->Guest();
  69. return;
  70. }
  71. // Check if the user is requesting a password reset.
  72. if (isset($_POST['email'])) {
  73. $this->emailReset = true;
  74. }
  75. // If the admin user is currently logged in, let them log in as anyone.
  76. else if (!$single_user &&
  77. isset($_SESSION['user']) && $_SESSION['user'] === 'admin') {
  78. $this->loggedIn = true;
  79. $this->defaultPage = true;
  80. if ($this->name !== 'admin') $from_admin = true;
  81. }
  82. else if ($this->Valid()) {
  83. $this->loggedIn = true;
  84. // Go to the user's default page when they first log in.
  85. $this->defaultPage = true;
  86. // Check if the user wants a persistent login.
  87. if (isset($_POST['remember']) && $_POST['remember'] === 'true') {
  88. $this->RememberLogin();
  89. }
  90. else {
  91. $this->ForgetLogin();
  92. }
  93. }
  94. else {
  95. $this->name = '';
  96. $this->group = '';
  97. $this->loginFailed = true;
  98. }
  99. // Restart the session in case the username has changed.
  100. session_destroy();
  101. session_start();
  102. // new-login is used by init.php to clear local storage.
  103. if ($this->defaultPage) $_SESSION['new-login'] = true;
  104. // Store from_admin in the new session.
  105. if ($from_admin) $_SESSION['from-admin'] = true;
  106. }
  107. // Check if a session already exists.
  108. else if (isset($_SESSION['user'])) {
  109. $this->name = $_SESSION['user'];
  110. $this->loggedIn = true;
  111. // Indieauth users are given persistent login by default.
  112. if (isset($_SESSION['indieauth-login'])) {
  113. unset($_SESSION['indieauth-login']);
  114. $this->RememberLogin();
  115. }
  116. }
  117. // Check if a persistent login cookie is being used.
  118. else if (isset($_COOKIE['token']) && isset($_COOKIE['user'])) {
  119. $this->CheckLogin();
  120. }
  121. // Check if a login code is being used last so that it can be removed.
  122. else if (isset($_GET['code'])) {
  123. $this->CheckCode();
  124. }
  125. if ($this->loggedIn) {
  126. // Don't update last visit for this user when logging in from admin.
  127. if (!isset($_SESSION['from-admin'])) $this->SetLastVisit();
  128. $this->SetGroup();
  129. $this->Settings();
  130. $this->config->SetUser($this->name);
  131. setcookie('user', $this->name, time() + 3600 * 24 * 7, '/');
  132. }
  133. }
  134. public function SetPermission($page, $owner = '') {
  135. $this->page = $page;
  136. $name = $owner === '' ? $page : $owner . '/' . $page;
  137. $this->canEditSite = can_edit_site();
  138. $this->canEditPage = can_edit_page($name);
  139. $this->canCopyPage = can_copy_page($name);
  140. $this->canViewPage = can_view_page($name);
  141. }
  142. // Private functions below here ////////////////////////////////////////////
  143. private function CheckCode() {
  144. // If a value isn't found set the login status as expired.
  145. $this->loginStatus = 'code expired';
  146. $mysqli = connect_db();
  147. // A login code can be sent to an email address in deploy.php after a
  148. // verification code was previously sent to make sure the email address
  149. // matches the requested domain. If a verification code is set here, make
  150. // sure it matches in DNS before checking the login code.
  151. $verification = '';
  152. $query = 'SELECT value FROM settings WHERE user = "admin" AND ' .
  153. 'label = "login" AND name = "verification"';
  154. if ($mysqli_result = $mysqli->query($query)) {
  155. if ($settings = $mysqli_result->fetch_assoc()) {
  156. $verification = $settings['value'];
  157. }
  158. $mysqli_result->close();
  159. }
  160. else {
  161. log_db('User->CheckCode 1: ' . $mysqli->error);
  162. }
  163. $verification_set = false;
  164. if ($verification !== '') {
  165. if ($result_list = dns_get_record($this->config->ServerName(), DNS_TXT)) {
  166. foreach ($result_list as $result) {
  167. if ($result['type'] === 'TXT' &&
  168. strtolower($result['txt']) === $verification) {
  169. $verification_set = true;
  170. break;
  171. }
  172. }
  173. }
  174. if (!$verification_set) {
  175. $this->loginStatus = 'verification not set';
  176. $this->loginFailed = true;
  177. }
  178. }
  179. if ($verification === '' || $verification_set) {
  180. $query = 'SELECT value FROM settings WHERE user = "admin" AND ' .
  181. 'label = "login" AND name = "code"';
  182. if ($mysqli_result = $mysqli->query($query)) {
  183. if ($settings = $mysqli_result->fetch_assoc()) {
  184. if ($settings['value'] === $_GET['code']) $this->loggedIn = true;
  185. else $this->loginStatus = 'code failed';
  186. }
  187. $mysqli_result->close();
  188. }
  189. else {
  190. log_db('User->CheckCode 2: ' . $mysqli->error);
  191. }
  192. if ($this->loggedIn) {
  193. $this->name = 'admin';
  194. $this->loginStatus = 'valid';
  195. $this->RememberLogin();
  196. // If the code worked then remove it now that the user is logged in.
  197. $query = 'DELETE FROM settings WHERE user = "admin" AND ' .
  198. 'label = "login" AND name = "code"';
  199. if (!$mysqli->query($query)) {
  200. log_db('User->CheckCode 3: ' . $mysqli->error);
  201. }
  202. }
  203. else {
  204. $this->loginFailed = true;
  205. }
  206. }
  207. $mysqli->close();
  208. }
  209. private function CheckLogin() {
  210. $mysqli = connect_db();
  211. $user = $mysqli->escape_string($_COOKIE['user']);
  212. list($series, $token) =
  213. explode(':', $mysqli->escape_string($_COOKIE['token']));
  214. $query = 'SELECT token FROM session WHERE user = "' . $user . '" AND ' .
  215. 'series = "' . $series . '"';
  216. if ($mysqli_result = $mysqli->query($query)) {
  217. if ($session = $mysqli_result->fetch_assoc()) {
  218. if ($session['token'] === $token) {
  219. $this->name = $user;
  220. $this->loggedIn = true;
  221. $_SESSION['limited-login'] = true;
  222. // Replace the token.
  223. $token = bin2hex(openssl_random_pseudo_bytes(16));
  224. $query = 'UPDATE session SET token = "' . $token . '" WHERE ' .
  225. 'user = "' . $user . '" AND series = "' . $series . '"';
  226. if (!$mysqli->query($query)) {
  227. log_db('User->CheckLogin 1: ' . $mysqli->error);
  228. }
  229. setcookie('token', $series . ':' . $token,
  230. time() + 3600 * 24 * 7, '/');
  231. }
  232. else {
  233. // When a token is provided for a series that doesn't match the
  234. // database, it means the token was stolen. Need to remove all
  235. // tokens in this series to prevent it being used again.
  236. $query = 'DELETE FROM session WHERE user = "' . $user . '" AND ' .
  237. 'series = "' . $series . '"';
  238. if (!$mysqli->query($query)) {
  239. log_db('User->CheckLogin 2: ' . $mysqli->error);
  240. }
  241. setcookie('user', '', time() - 3600, '/');
  242. setcookie('token', '', time() - 3600, '/');
  243. session_destroy();
  244. }
  245. }
  246. $mysqli_result->close();
  247. }
  248. else {
  249. log_db('User->CheckLogin 3: ' . $mysqli->error);
  250. }
  251. $mysqli->close();
  252. }
  253. private function ForgetLogin() {
  254. $mysqli = connect_db();
  255. $query = 'DELETE FROM session WHERE user = "' . $this->name . '"';
  256. if (!$mysqli->query($query)) {
  257. log_db('User->ForgetLogin: ' . $mysqli->error);
  258. }
  259. $mysqli->close();
  260. }
  261. private function Guest() {
  262. $this->name = 'guest' . time();
  263. $chars = 'bcdfghjklmnpqrstvwxyz1234567890';
  264. $length = strlen($chars) - 1;
  265. for ($i = 0; $i < 5; $i++) {
  266. $this->name .= substr($chars, mt_rand(0, $length), 1);
  267. }
  268. if (new_user($this, 'admin') === true) {
  269. $this->loggedIn = true;
  270. if (isset($_POST['remember']) && $_POST['remember'] === 'true') {
  271. $this->RememberLogin();
  272. }
  273. else {
  274. $this->ForgetLogin();
  275. }
  276. setcookie('user', $this->name, time() + 3600 * 24 * 7, '/');
  277. }
  278. else {
  279. $this->name = '';
  280. $this->loggedIn = false;
  281. }
  282. }
  283. private function RememberLogin() {
  284. $mysqli = connect_db();
  285. // When a user logs in and a persistent login cookie already exists,
  286. // remove the existing one from the database first.
  287. if (isset($_COOKIE['user']) && isset($_COOKIE['token'])) {
  288. $user = $mysqli->escape_string($_COOKIE['user']);
  289. list($series, $token) =
  290. explode(':', $mysqli->escape_string($_COOKIE['token']));
  291. $query = 'DELETE FROM session WHERE user = "' . $user . '" AND ' .
  292. 'series = "' . $series . '" AND token = "' . $token . '"';
  293. if (!$mysqli->query($query)) {
  294. log_db('User->RememberLogin 1: ' . $mysqli->error);
  295. }
  296. }
  297. $series = bin2hex(openssl_random_pseudo_bytes(16));
  298. $token = bin2hex(openssl_random_pseudo_bytes(16));
  299. $query = 'INSERT INTO session VALUES ("' . $this->name . '", ' .
  300. '"' . $series . '", "' . $token . '")';
  301. if (!$mysqli->query($query)) {
  302. log_db('User->RememberLogin 2: ' . $mysqli->error);
  303. }
  304. $mysqli->close();
  305. setcookie('token', $series . ':' . $token, time() + 3600 * 24 * 7, '/');
  306. }
  307. private function SetGroup() {
  308. $mysqli = connect_db();
  309. $query = 'SELECT system_group, active FROM users WHERE ' .
  310. 'user = "' . $this->name . '"';
  311. if ($mysqli_result = $mysqli->query($query)) {
  312. if ($users = $mysqli_result->fetch_assoc()) {
  313. $this->group = $users['system_group'];
  314. $this->active = $users['active'] === '1';
  315. }
  316. $mysqli_result->close();
  317. }
  318. else {
  319. log_db('User->SetGroup: ' . $mysqli->error);
  320. }
  321. $mysqli->close();
  322. }
  323. private function SetLastVisit() {
  324. $mysqli = connect_db();
  325. $query = 'INSERT INTO settings VALUES ("' . $this->name . '", ' .
  326. '"account", "lastVisit", "' . time() . '") ON DUPLICATE KEY UPDATE ' .
  327. 'value = "' . time() . '"';
  328. if (!$mysqli->query($query)) {
  329. log_db('User->SetLastVisit: ' . $mysqli->error);
  330. }
  331. $mysqli->close();
  332. }
  333. private function Settings() {
  334. // The start module provides some default settings if installed. It is
  335. // possible that module.php cannot be included before user.php, so need
  336. // to check that the Module class exits here.
  337. if (class_exists('Module')) {
  338. $start = new Module($this, $this->name, 'start');
  339. if ($start->IsInstalled()) {
  340. $this->settings = $start->Factory('Settings');
  341. }
  342. }
  343. $mysqli = connect_db();
  344. $query = 'SELECT label, name, value FROM settings WHERE ' .
  345. 'user = "' . $this->name . '"';
  346. if ($mysqli_result = $mysqli->query($query)) {
  347. while ($settings = $mysqli_result->fetch_assoc()) {
  348. $label = $settings['label'];
  349. if (isset($this->settings[$label])) {
  350. $this->settings[$label][$settings['name']] = $settings['value'];
  351. }
  352. else {
  353. $this->settings[$label] = [$settings['name'] => $settings['value']];
  354. }
  355. }
  356. $mysqli_result->close();
  357. }
  358. else {
  359. log_db('User->Settings: ' . $mysqli->error);
  360. }
  361. $mysqli->close();
  362. }
  363. private function Valid() {
  364. $valid = false;
  365. $us_password = isset($_POST['password']) ? trim($_POST['password']) : '';
  366. $mysqli = connect_db();
  367. $query = 'SELECT password, confirmed FROM users WHERE ' .
  368. 'user = "' . $this->name . '"';
  369. if ($mysqli_result = $mysqli->query($query)) {
  370. if ($users = $mysqli_result->fetch_assoc()) {
  371. if (password_verify($us_password, $users['password'])) {
  372. // Set loginStatus so that the Login module can display a better
  373. // error message if login failed.
  374. if ($users['confirmed'] === '1') {
  375. $valid = true;
  376. $this->loginStatus = 'valid';
  377. }
  378. else {
  379. $this->loginStatus = 'unconfirmed';
  380. }
  381. }
  382. else {
  383. $this->loginStatus = 'password failed';
  384. }
  385. }
  386. else {
  387. $this->loginStatus = 'user not found';
  388. }
  389. $mysqli_result->close();
  390. }
  391. else {
  392. log_db('User->Valid: ' . $mysqli->error);
  393. }
  394. $mysqli->close();
  395. return $valid;
  396. }
  397. }