auth.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. <?php
  2. use dokuwiki\Logger;
  3. use dokuwiki\Utf8\Sort;
  4. /**
  5. * Plaintext authentication backend
  6. *
  7. * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
  8. * @author Andreas Gohr <andi@splitbrain.org>
  9. * @author Chris Smith <chris@jalakai.co.uk>
  10. * @author Jan Schumann <js@schumann-it.com>
  11. */
  12. class auth_plugin_authplain extends DokuWiki_Auth_Plugin
  13. {
  14. /** @var array user cache */
  15. protected $users = null;
  16. /** @var array filter pattern */
  17. protected $pattern = array();
  18. /** @var bool safe version of preg_split */
  19. protected $pregsplit_safe = false;
  20. /**
  21. * Constructor
  22. *
  23. * Carry out sanity checks to ensure the object is
  24. * able to operate. Set capabilities.
  25. *
  26. * @author Christopher Smith <chris@jalakai.co.uk>
  27. */
  28. public function __construct()
  29. {
  30. parent::__construct();
  31. global $config_cascade;
  32. if (!@is_readable($config_cascade['plainauth.users']['default'])) {
  33. $this->success = false;
  34. } else {
  35. if (@is_writable($config_cascade['plainauth.users']['default'])) {
  36. $this->cando['addUser'] = true;
  37. $this->cando['delUser'] = true;
  38. $this->cando['modLogin'] = true;
  39. $this->cando['modPass'] = true;
  40. $this->cando['modName'] = true;
  41. $this->cando['modMail'] = true;
  42. $this->cando['modGroups'] = true;
  43. }
  44. $this->cando['getUsers'] = true;
  45. $this->cando['getUserCount'] = true;
  46. $this->cando['getGroups'] = true;
  47. }
  48. }
  49. /**
  50. * Check user+password
  51. *
  52. * Checks if the given user exists and the given
  53. * plaintext password is correct
  54. *
  55. * @author Andreas Gohr <andi@splitbrain.org>
  56. * @param string $user
  57. * @param string $pass
  58. * @return bool
  59. */
  60. public function checkPass($user, $pass)
  61. {
  62. $userinfo = $this->getUserData($user);
  63. if ($userinfo === false) return false;
  64. return auth_verifyPassword($pass, $this->users[$user]['pass']);
  65. }
  66. /**
  67. * Return user info
  68. *
  69. * Returns info about the given user needs to contain
  70. * at least these fields:
  71. *
  72. * name string full name of the user
  73. * mail string email addres of the user
  74. * grps array list of groups the user is in
  75. *
  76. * @author Andreas Gohr <andi@splitbrain.org>
  77. * @param string $user
  78. * @param bool $requireGroups (optional) ignored by this plugin, grps info always supplied
  79. * @return array|false
  80. */
  81. public function getUserData($user, $requireGroups = true)
  82. {
  83. if ($this->users === null) $this->loadUserData();
  84. return isset($this->users[$user]) ? $this->users[$user] : false;
  85. }
  86. /**
  87. * Creates a string suitable for saving as a line
  88. * in the file database
  89. * (delimiters escaped, etc.)
  90. *
  91. * @param string $user
  92. * @param string $pass
  93. * @param string $name
  94. * @param string $mail
  95. * @param array $grps list of groups the user is in
  96. * @return string
  97. */
  98. protected function createUserLine($user, $pass, $name, $mail, $grps)
  99. {
  100. $groups = join(',', $grps);
  101. $userline = array($user, $pass, $name, $mail, $groups);
  102. $userline = str_replace('\\', '\\\\', $userline); // escape \ as \\
  103. $userline = str_replace(':', '\\:', $userline); // escape : as \:
  104. $userline = join(':', $userline)."\n";
  105. return $userline;
  106. }
  107. /**
  108. * Create a new User
  109. *
  110. * Returns false if the user already exists, null when an error
  111. * occurred and true if everything went well.
  112. *
  113. * The new user will be added to the default group by this
  114. * function if grps are not specified (default behaviour).
  115. *
  116. * @author Andreas Gohr <andi@splitbrain.org>
  117. * @author Chris Smith <chris@jalakai.co.uk>
  118. *
  119. * @param string $user
  120. * @param string $pwd
  121. * @param string $name
  122. * @param string $mail
  123. * @param array $grps
  124. * @return bool|null|string
  125. */
  126. public function createUser($user, $pwd, $name, $mail, $grps = null)
  127. {
  128. global $conf;
  129. global $config_cascade;
  130. // user mustn't already exist
  131. if ($this->getUserData($user) !== false) {
  132. msg($this->getLang('userexists'), -1);
  133. return false;
  134. }
  135. $pass = auth_cryptPassword($pwd);
  136. // set default group if no groups specified
  137. if (!is_array($grps)) $grps = array($conf['defaultgroup']);
  138. // prepare user line
  139. $userline = $this->createUserLine($user, $pass, $name, $mail, $grps);
  140. if (!io_saveFile($config_cascade['plainauth.users']['default'], $userline, true)) {
  141. msg($this->getLang('writefail'), -1);
  142. return null;
  143. }
  144. $this->users[$user] = compact('pass', 'name', 'mail', 'grps');
  145. return $pwd;
  146. }
  147. /**
  148. * Modify user data
  149. *
  150. * @author Chris Smith <chris@jalakai.co.uk>
  151. * @param string $user nick of the user to be changed
  152. * @param array $changes array of field/value pairs to be changed (password will be clear text)
  153. * @return bool
  154. */
  155. public function modifyUser($user, $changes)
  156. {
  157. global $ACT;
  158. global $config_cascade;
  159. // sanity checks, user must already exist and there must be something to change
  160. if (($userinfo = $this->getUserData($user)) === false) {
  161. msg($this->getLang('usernotexists'), -1);
  162. return false;
  163. }
  164. // don't modify protected users
  165. if (!empty($userinfo['protected'])) {
  166. msg(sprintf($this->getLang('protected'), hsc($user)), -1);
  167. return false;
  168. }
  169. if (!is_array($changes) || !count($changes)) return true;
  170. // update userinfo with new data, remembering to encrypt any password
  171. $newuser = $user;
  172. foreach ($changes as $field => $value) {
  173. if ($field == 'user') {
  174. $newuser = $value;
  175. continue;
  176. }
  177. if ($field == 'pass') $value = auth_cryptPassword($value);
  178. $userinfo[$field] = $value;
  179. }
  180. $userline = $this->createUserLine(
  181. $newuser,
  182. $userinfo['pass'],
  183. $userinfo['name'],
  184. $userinfo['mail'],
  185. $userinfo['grps']
  186. );
  187. if (!io_replaceInFile($config_cascade['plainauth.users']['default'], '/^'.$user.':/', $userline, true)) {
  188. msg('There was an error modifying your user data. You may need to register again.', -1);
  189. // FIXME, io functions should be fail-safe so existing data isn't lost
  190. $ACT = 'register';
  191. return false;
  192. }
  193. if(isset($this->users[$user])) unset($this->users[$user]);
  194. $this->users[$newuser] = $userinfo;
  195. return true;
  196. }
  197. /**
  198. * Remove one or more users from the list of registered users
  199. *
  200. * @author Christopher Smith <chris@jalakai.co.uk>
  201. * @param array $users array of users to be deleted
  202. * @return int the number of users deleted
  203. */
  204. public function deleteUsers($users)
  205. {
  206. global $config_cascade;
  207. if (!is_array($users) || empty($users)) return 0;
  208. if ($this->users === null) $this->loadUserData();
  209. $deleted = array();
  210. foreach ($users as $user) {
  211. // don't delete protected users
  212. if (!empty($this->users[$user]['protected'])) {
  213. msg(sprintf($this->getLang('protected'), hsc($user)), -1);
  214. continue;
  215. }
  216. if (isset($this->users[$user])) $deleted[] = preg_quote($user, '/');
  217. }
  218. if (empty($deleted)) return 0;
  219. $pattern = '/^('.join('|', $deleted).'):/';
  220. if (!io_deleteFromFile($config_cascade['plainauth.users']['default'], $pattern, true)) {
  221. msg($this->getLang('writefail'), -1);
  222. return 0;
  223. }
  224. // reload the user list and count the difference
  225. $count = count($this->users);
  226. $this->loadUserData();
  227. $count -= count($this->users);
  228. return $count;
  229. }
  230. /**
  231. * Return a count of the number of user which meet $filter criteria
  232. *
  233. * @author Chris Smith <chris@jalakai.co.uk>
  234. *
  235. * @param array $filter
  236. * @return int
  237. */
  238. public function getUserCount($filter = array())
  239. {
  240. if ($this->users === null) $this->loadUserData();
  241. if (!count($filter)) return count($this->users);
  242. $count = 0;
  243. $this->constructPattern($filter);
  244. foreach ($this->users as $user => $info) {
  245. $count += $this->filter($user, $info);
  246. }
  247. return $count;
  248. }
  249. /**
  250. * Bulk retrieval of user data
  251. *
  252. * @author Chris Smith <chris@jalakai.co.uk>
  253. *
  254. * @param int $start index of first user to be returned
  255. * @param int $limit max number of users to be returned
  256. * @param array $filter array of field/pattern pairs
  257. * @return array userinfo (refer getUserData for internal userinfo details)
  258. */
  259. public function retrieveUsers($start = 0, $limit = 0, $filter = array())
  260. {
  261. if ($this->users === null) $this->loadUserData();
  262. Sort::ksort($this->users);
  263. $i = 0;
  264. $count = 0;
  265. $out = array();
  266. $this->constructPattern($filter);
  267. foreach ($this->users as $user => $info) {
  268. if ($this->filter($user, $info)) {
  269. if ($i >= $start) {
  270. $out[$user] = $info;
  271. $count++;
  272. if (($limit > 0) && ($count >= $limit)) break;
  273. }
  274. $i++;
  275. }
  276. }
  277. return $out;
  278. }
  279. /**
  280. * Retrieves groups.
  281. * Loads complete user data into memory before searching for groups.
  282. *
  283. * @param int $start index of first group to be returned
  284. * @param int $limit max number of groups to be returned
  285. * @return array
  286. */
  287. public function retrieveGroups($start = 0, $limit = 0)
  288. {
  289. $groups = [];
  290. if ($this->users === null) $this->loadUserData();
  291. foreach($this->users as $user => $info) {
  292. $groups = array_merge($groups, array_diff($info['grps'], $groups));
  293. }
  294. Sort::ksort($groups);
  295. if($limit > 0) {
  296. return array_splice($groups, $start, $limit);
  297. }
  298. return array_splice($groups, $start);
  299. }
  300. /**
  301. * Only valid pageid's (no namespaces) for usernames
  302. *
  303. * @param string $user
  304. * @return string
  305. */
  306. public function cleanUser($user)
  307. {
  308. global $conf;
  309. return cleanID(str_replace([':', '/', ';'], $conf['sepchar'], $user));
  310. }
  311. /**
  312. * Only valid pageid's (no namespaces) for groupnames
  313. *
  314. * @param string $group
  315. * @return string
  316. */
  317. public function cleanGroup($group)
  318. {
  319. global $conf;
  320. return cleanID(str_replace([':', '/', ';'], $conf['sepchar'], $group));
  321. }
  322. /**
  323. * Load all user data
  324. *
  325. * loads the user file into a datastructure
  326. *
  327. * @author Andreas Gohr <andi@splitbrain.org>
  328. */
  329. protected function loadUserData()
  330. {
  331. global $config_cascade;
  332. $this->users = $this->readUserFile($config_cascade['plainauth.users']['default']);
  333. // support protected users
  334. if (!empty($config_cascade['plainauth.users']['protected'])) {
  335. $protected = $this->readUserFile($config_cascade['plainauth.users']['protected']);
  336. foreach (array_keys($protected) as $key) {
  337. $protected[$key]['protected'] = true;
  338. }
  339. $this->users = array_merge($this->users, $protected);
  340. }
  341. }
  342. /**
  343. * Read user data from given file
  344. *
  345. * ignores non existing files
  346. *
  347. * @param string $file the file to load data from
  348. * @return array
  349. */
  350. protected function readUserFile($file)
  351. {
  352. $users = array();
  353. if (!file_exists($file)) return $users;
  354. $lines = file($file);
  355. foreach ($lines as $line) {
  356. $line = preg_replace('/#.*$/', '', $line); //ignore comments
  357. $line = trim($line);
  358. if (empty($line)) continue;
  359. $row = $this->splitUserData($line);
  360. $row = str_replace('\\:', ':', $row);
  361. $row = str_replace('\\\\', '\\', $row);
  362. $groups = array_values(array_filter(explode(",", $row[4])));
  363. $users[$row[0]]['pass'] = $row[1];
  364. $users[$row[0]]['name'] = urldecode($row[2]);
  365. $users[$row[0]]['mail'] = $row[3];
  366. $users[$row[0]]['grps'] = $groups;
  367. }
  368. return $users;
  369. }
  370. /**
  371. * Get the user line split into it's parts
  372. *
  373. * @param string $line
  374. * @return string[]
  375. */
  376. protected function splitUserData($line)
  377. {
  378. $data = preg_split('/(?<![^\\\\]\\\\)\:/', $line, 5); // allow for : escaped as \:
  379. if(count($data) < 5) {
  380. $data = array_pad($data, 5, '');
  381. Logger::error('User line with less than 5 fields. Possibly corruption in your user file', $data);
  382. }
  383. return $data;
  384. }
  385. /**
  386. * return true if $user + $info match $filter criteria, false otherwise
  387. *
  388. * @author Chris Smith <chris@jalakai.co.uk>
  389. *
  390. * @param string $user User login
  391. * @param array $info User's userinfo array
  392. * @return bool
  393. */
  394. protected function filter($user, $info)
  395. {
  396. foreach ($this->pattern as $item => $pattern) {
  397. if ($item == 'user') {
  398. if (!preg_match($pattern, $user)) return false;
  399. } elseif ($item == 'grps') {
  400. if (!count(preg_grep($pattern, $info['grps']))) return false;
  401. } else {
  402. if (!preg_match($pattern, $info[$item])) return false;
  403. }
  404. }
  405. return true;
  406. }
  407. /**
  408. * construct a filter pattern
  409. *
  410. * @param array $filter
  411. */
  412. protected function constructPattern($filter)
  413. {
  414. $this->pattern = array();
  415. foreach ($filter as $item => $pattern) {
  416. $this->pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters
  417. }
  418. }
  419. }