You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

704 lines
25 KiB

  1. <?php
  2. use dokuwiki\Extension\AuthPlugin;
  3. use dokuwiki\PassHash;
  4. use dokuwiki\Utf8\Sort;
  5. /**
  6. * LDAP authentication backend
  7. *
  8. * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
  9. * @author Andreas Gohr <andi@splitbrain.org>
  10. * @author Chris Smith <chris@jalakaic.co.uk>
  11. * @author Jan Schumann <js@schumann-it.com>
  12. */
  13. class auth_plugin_authldap extends AuthPlugin
  14. {
  15. /* @var resource $con holds the LDAP connection */
  16. protected $con;
  17. /* @var int $bound What type of connection does already exist? */
  18. protected $bound = 0; // 0: anonymous, 1: user, 2: superuser
  19. /* @var array $users User data cache */
  20. protected $users;
  21. /* @var array $pattern User filter pattern */
  22. protected $pattern;
  23. /**
  24. * Constructor
  25. */
  26. public function __construct()
  27. {
  28. parent::__construct();
  29. // ldap extension is needed
  30. if (!function_exists('ldap_connect')) {
  31. $this->debug("LDAP err: PHP LDAP extension not found.", -1, __LINE__, __FILE__);
  32. $this->success = false;
  33. return;
  34. }
  35. // Add the capabilities to change the password
  36. $this->cando['modPass'] = $this->getConf('modPass');
  37. }
  38. /**
  39. * Check user+password
  40. *
  41. * Checks if the given user exists and the given
  42. * plaintext password is correct by trying to bind
  43. * to the LDAP server
  44. *
  45. * @param string $user
  46. * @param string $pass
  47. * @return bool
  48. * @author Andreas Gohr <andi@splitbrain.org>
  49. */
  50. public function checkPass($user, $pass)
  51. {
  52. // reject empty password
  53. if (empty($pass)) return false;
  54. if (!$this->openLDAP()) return false;
  55. // indirect user bind
  56. if ($this->getConf('binddn') && $this->getConf('bindpw')) {
  57. // use superuser credentials
  58. if (!@ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')))) {
  59. $this->debug('LDAP bind as superuser: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  60. return false;
  61. }
  62. $this->bound = 2;
  63. } elseif (
  64. $this->getConf('binddn') &&
  65. $this->getConf('usertree') &&
  66. $this->getConf('userfilter')
  67. ) {
  68. // special bind string
  69. $dn = $this->makeFilter(
  70. $this->getConf('binddn'),
  71. ['user' => $user, 'server' => $this->getConf('server')]
  72. );
  73. } elseif (strpos($this->getConf('usertree'), '%{user}')) {
  74. // direct user bind
  75. $dn = $this->makeFilter(
  76. $this->getConf('usertree'),
  77. ['user' => $user, 'server' => $this->getConf('server')]
  78. );
  79. } elseif (!@ldap_bind($this->con)) {
  80. // Anonymous bind
  81. msg("LDAP: can not bind anonymously", -1);
  82. $this->debug('LDAP anonymous bind: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  83. return false;
  84. }
  85. // Try to bind to with the dn if we have one.
  86. if (!empty($dn)) {
  87. // User/Password bind
  88. if (!@ldap_bind($this->con, $dn, $pass)) {
  89. $this->debug("LDAP: bind with $dn failed", -1, __LINE__, __FILE__);
  90. $this->debug('LDAP user dn bind: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  91. return false;
  92. }
  93. $this->bound = 1;
  94. return true;
  95. } else {
  96. // See if we can find the user
  97. $info = $this->fetchUserData($user, true);
  98. if (empty($info['dn'])) {
  99. return false;
  100. } else {
  101. $dn = $info['dn'];
  102. }
  103. // Try to bind with the dn provided
  104. if (!@ldap_bind($this->con, $dn, $pass)) {
  105. $this->debug("LDAP: bind with $dn failed", -1, __LINE__, __FILE__);
  106. $this->debug('LDAP user bind: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  107. return false;
  108. }
  109. $this->bound = 1;
  110. return true;
  111. }
  112. }
  113. /**
  114. * Return user info
  115. *
  116. * Returns info about the given user needs to contain
  117. * at least these fields:
  118. *
  119. * name string full name of the user
  120. * mail string email addres of the user
  121. * grps array list of groups the user is in
  122. *
  123. * This LDAP specific function returns the following
  124. * addional fields:
  125. *
  126. * dn string distinguished name (DN)
  127. * uid string Posix User ID
  128. * inbind bool for internal use - avoid loop in binding
  129. *
  130. * @param string $user
  131. * @param bool $requireGroups (optional) - ignored, groups are always supplied by this plugin
  132. * @return array containing user data or false
  133. * @author <evaldas.auryla@pheur.org>
  134. * @author Stephane Chazelas <stephane.chazelas@emerson.com>
  135. * @author Steffen Schoch <schoch@dsb.net>
  136. *
  137. * @author Andreas Gohr <andi@splitbrain.org>
  138. * @author Trouble
  139. * @author Dan Allen <dan.j.allen@gmail.com>
  140. */
  141. public function getUserData($user, $requireGroups = true)
  142. {
  143. return $this->fetchUserData($user);
  144. }
  145. /**
  146. * @param string $user
  147. * @param bool $inbind authldap specific, true if in bind phase
  148. * @return array containing user data or false
  149. */
  150. protected function fetchUserData($user, $inbind = false)
  151. {
  152. global $conf;
  153. if (!$this->openLDAP()) return [];
  154. // force superuser bind if wanted and not bound as superuser yet
  155. if ($this->getConf('binddn') && $this->getConf('bindpw') && $this->bound < 2) {
  156. // use superuser credentials
  157. if (!@ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')))) {
  158. $this->debug('LDAP bind as superuser: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  159. return [];
  160. }
  161. $this->bound = 2;
  162. } elseif ($this->bound == 0 && !$inbind) {
  163. // in some cases getUserData is called outside the authentication workflow
  164. // eg. for sending email notification on subscribed pages. This data might not
  165. // be accessible anonymously, so we try to rebind the current user here
  166. [$loginuser, $loginsticky, $loginpass] = auth_getCookie();
  167. if ($loginuser && $loginpass) {
  168. $loginpass = auth_decrypt($loginpass, auth_cookiesalt(!$loginsticky, true));
  169. $this->checkPass($loginuser, $loginpass);
  170. }
  171. }
  172. $info = [];
  173. $info['user'] = $user;
  174. $this->debug('LDAP user to find: ' . hsc($info['user']), 0, __LINE__, __FILE__);
  175. $info['server'] = $this->getConf('server');
  176. $this->debug('LDAP Server: ' . hsc($info['server']), 0, __LINE__, __FILE__);
  177. //get info for given user
  178. $base = $this->makeFilter($this->getConf('usertree'), $info);
  179. if ($this->getConf('userfilter')) {
  180. $filter = $this->makeFilter($this->getConf('userfilter'), $info);
  181. } else {
  182. $filter = "(ObjectClass=*)";
  183. }
  184. $this->debug('LDAP Filter: ' . hsc($filter), 0, __LINE__, __FILE__);
  185. $this->debug('LDAP user search: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  186. $this->debug('LDAP search at: ' . hsc($base . ' ' . $filter), 0, __LINE__, __FILE__);
  187. $sr = $this->ldapSearch($this->con, $base, $filter, $this->getConf('userscope'), $this->getConf('attributes'));
  188. if ($sr === false) {
  189. $this->debug('User ldap_search failed. Check configuration.', 0, __LINE__, __FILE__);
  190. return false;
  191. }
  192. $result = @ldap_get_entries($this->con, $sr);
  193. // if result is not an array
  194. if (!is_array($result)) {
  195. // no objects found
  196. $this->debug('LDAP search returned non-array result: ' . hsc(print($result)), -1, __LINE__, __FILE__);
  197. return [];
  198. }
  199. // Don't accept more or less than one response
  200. if ($result['count'] != 1) {
  201. $this->debug(
  202. 'LDAP search returned ' . hsc($result['count']) . ' results while it should return 1!',
  203. -1,
  204. __LINE__,
  205. __FILE__
  206. );
  207. //for($i = 0; $i < $result["count"]; $i++) {
  208. //$this->_debug('result: '.hsc(print_r($result[$i])), 0, __LINE__, __FILE__);
  209. //}
  210. return [];
  211. }
  212. $this->debug('LDAP search found single result !', 0, __LINE__, __FILE__);
  213. $user_result = $result[0];
  214. ldap_free_result($sr);
  215. // general user info
  216. $info['dn'] = $user_result['dn'];
  217. $info['gid'] = $user_result['gidnumber'][0] ?? null;
  218. $info['mail'] = $user_result['mail'][0];
  219. $info['name'] = $user_result['cn'][0];
  220. $info['grps'] = [];
  221. // overwrite if other attribs are specified.
  222. if (is_array($this->getConf('mapping'))) {
  223. foreach ($this->getConf('mapping') as $localkey => $key) {
  224. if (is_array($key)) {
  225. // use regexp to clean up user_result
  226. // $key = array($key=>$regexp), only handles the first key-value
  227. $regexp = current($key);
  228. $key = key($key);
  229. if ($user_result[$key]) foreach ($user_result[$key] as $grpkey => $grp) {
  230. if ($grpkey !== 'count' && preg_match($regexp, $grp, $match)) {
  231. if ($localkey == 'grps') {
  232. $info[$localkey][] = $match[1];
  233. } else {
  234. $info[$localkey] = $match[1];
  235. }
  236. }
  237. }
  238. } else {
  239. $info[$localkey] = $user_result[$key][0];
  240. }
  241. }
  242. }
  243. $user_result = array_merge($info, $user_result);
  244. //get groups for given user if grouptree is given
  245. if ($this->getConf('grouptree') || $this->getConf('groupfilter')) {
  246. $base = $this->makeFilter($this->getConf('grouptree'), $user_result);
  247. $filter = $this->makeFilter($this->getConf('groupfilter'), $user_result);
  248. $sr = $this->ldapSearch(
  249. $this->con,
  250. $base,
  251. $filter,
  252. $this->getConf('groupscope'),
  253. [$this->getConf('groupkey')]
  254. );
  255. $this->debug('LDAP group search: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  256. $this->debug('LDAP search at: ' . hsc($base . ' ' . $filter), 0, __LINE__, __FILE__);
  257. if (!$sr) {
  258. msg("LDAP: Reading group memberships failed", -1);
  259. return [];
  260. }
  261. $result = ldap_get_entries($this->con, $sr);
  262. ldap_free_result($sr);
  263. if (is_array($result)) foreach ($result as $grp) {
  264. if (!empty($grp[$this->getConf('groupkey')])) {
  265. $group = $grp[$this->getConf('groupkey')];
  266. if (is_array($group)) {
  267. $group = $group[0];
  268. } else {
  269. $this->debug('groupkey did not return a detailled result', 0, __LINE__, __FILE__);
  270. }
  271. if ($group === '') continue;
  272. $this->debug('LDAP usergroup: ' . hsc($group), 0, __LINE__, __FILE__);
  273. $info['grps'][] = $group;
  274. }
  275. }
  276. }
  277. // always add the default group to the list of groups
  278. if (!$info['grps'] || !in_array($conf['defaultgroup'], $info['grps'])) {
  279. $info['grps'][] = $conf['defaultgroup'];
  280. }
  281. return $info;
  282. }
  283. /**
  284. * Definition of the function modifyUser in order to modify the password
  285. *
  286. * @param string $user nick of the user to be changed
  287. * @param array $changes array of field/value pairs to be changed (password will be clear text)
  288. * @return bool true on success, false on error
  289. */
  290. public function modifyUser($user, $changes)
  291. {
  292. // open the connection to the ldap
  293. if (!$this->openLDAP()) {
  294. $this->debug('LDAP cannot connect: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  295. return false;
  296. }
  297. // find the information about the user, in particular the "dn"
  298. $info = $this->getUserData($user, true);
  299. if (empty($info['dn'])) {
  300. $this->debug('LDAP cannot find your user dn', 0, __LINE__, __FILE__);
  301. return false;
  302. }
  303. $dn = $info['dn'];
  304. // find the old password of the user
  305. [$loginuser, $loginsticky, $loginpass] = auth_getCookie();
  306. if ($loginuser !== null) { // the user is currently logged in
  307. $secret = auth_cookiesalt(!$loginsticky, true);
  308. $pass = auth_decrypt($loginpass, $secret);
  309. // bind with the ldap
  310. if (!@ldap_bind($this->con, $dn, $pass)) {
  311. $this->debug(
  312. 'LDAP user bind failed: ' . hsc($dn) . ': ' . hsc(ldap_error($this->con)),
  313. 0,
  314. __LINE__,
  315. __FILE__
  316. );
  317. return false;
  318. }
  319. } elseif ($this->getConf('binddn') && $this->getConf('bindpw')) {
  320. // we are changing the password on behalf of the user (eg: forgotten password)
  321. // bind with the superuser ldap
  322. if (!@ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')))) {
  323. $this->debug('LDAP bind as superuser: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  324. return false;
  325. }
  326. } else {
  327. return false; // no otherway
  328. }
  329. // Generate the salted hashed password for LDAP
  330. $phash = new PassHash();
  331. $hash = $phash->hash_ssha($changes['pass']);
  332. // change the password
  333. if (!@ldap_mod_replace($this->con, $dn, ['userpassword' => $hash])) {
  334. $this->debug(
  335. 'LDAP mod replace failed: ' . hsc($dn) . ': ' . hsc(ldap_error($this->con)),
  336. 0,
  337. __LINE__,
  338. __FILE__
  339. );
  340. return false;
  341. }
  342. return true;
  343. }
  344. /**
  345. * Most values in LDAP are case-insensitive
  346. *
  347. * @return bool
  348. */
  349. public function isCaseSensitive()
  350. {
  351. return false;
  352. }
  353. /**
  354. * Bulk retrieval of user data
  355. *
  356. * @param int $start index of first user to be returned
  357. * @param int $limit max number of users to be returned
  358. * @param array $filter array of field/pattern pairs, null for no filter
  359. * @return array of userinfo (refer getUserData for internal userinfo details)
  360. * @author Dominik Eckelmann <dokuwiki@cosmocode.de>
  361. */
  362. public function retrieveUsers($start = 0, $limit = 0, $filter = [])
  363. {
  364. if (!$this->openLDAP()) return [];
  365. if (is_null($this->users)) {
  366. // Perform the search and grab all their details
  367. if ($this->getConf('userfilter')) {
  368. $all_filter = str_replace('%{user}', '*', $this->getConf('userfilter'));
  369. } else {
  370. $all_filter = "(ObjectClass=*)";
  371. }
  372. $sr = ldap_search($this->con, $this->getConf('usertree'), $all_filter);
  373. $entries = ldap_get_entries($this->con, $sr);
  374. $users_array = [];
  375. $userkey = $this->getConf('userkey');
  376. for ($i = 0; $i < $entries["count"]; $i++) {
  377. $users_array[] = $entries[$i][$userkey][0];
  378. }
  379. Sort::asort($users_array);
  380. $result = $users_array;
  381. if (!$result) return [];
  382. $this->users = array_fill_keys($result, false);
  383. }
  384. $i = 0;
  385. $count = 0;
  386. $this->constructPattern($filter);
  387. $result = [];
  388. foreach ($this->users as $user => &$info) {
  389. if ($i++ < $start) {
  390. continue;
  391. }
  392. if ($info === false) {
  393. $info = $this->getUserData($user);
  394. }
  395. if ($this->filter($user, $info)) {
  396. $result[$user] = $info;
  397. if (($limit > 0) && (++$count >= $limit)) break;
  398. }
  399. }
  400. return $result;
  401. }
  402. /**
  403. * Make LDAP filter strings.
  404. *
  405. * Used by auth_getUserData to make the filter
  406. * strings for grouptree and groupfilter
  407. *
  408. * @param string $filter ldap search filter with placeholders
  409. * @param array $placeholders placeholders to fill in
  410. * @return string
  411. * @author Troels Liebe Bentsen <tlb@rapanden.dk>
  412. */
  413. protected function makeFilter($filter, $placeholders)
  414. {
  415. preg_match_all("/%{([^}]+)/", $filter, $matches, PREG_PATTERN_ORDER);
  416. //replace each match
  417. foreach ($matches[1] as $match) {
  418. //take first element if array
  419. if (is_array($placeholders[$match])) {
  420. $value = $placeholders[$match][0];
  421. } else {
  422. $value = $placeholders[$match];
  423. }
  424. $value = $this->filterEscape($value);
  425. $filter = str_replace('%{' . $match . '}', $value, $filter);
  426. }
  427. return $filter;
  428. }
  429. /**
  430. * return true if $user + $info match $filter criteria, false otherwise
  431. *
  432. * @param string $user the user's login name
  433. * @param array $info the user's userinfo array
  434. * @return bool
  435. * @author Chris Smith <chris@jalakai.co.uk>
  436. *
  437. */
  438. protected function filter($user, $info)
  439. {
  440. foreach ($this->pattern as $item => $pattern) {
  441. if ($item == 'user') {
  442. if (!preg_match($pattern, $user)) return false;
  443. } elseif ($item == 'grps') {
  444. if (!count(preg_grep($pattern, $info['grps']))) return false;
  445. } elseif (!preg_match($pattern, $info[$item])) {
  446. return false;
  447. }
  448. }
  449. return true;
  450. }
  451. /**
  452. * Set the filter pattern
  453. *
  454. * @param $filter
  455. * @return void
  456. * @author Chris Smith <chris@jalakai.co.uk>
  457. *
  458. */
  459. protected function constructPattern($filter)
  460. {
  461. $this->pattern = [];
  462. foreach ($filter as $item => $pattern) {
  463. $this->pattern[$item] = '/' . str_replace('/', '\/', $pattern) . '/i'; // allow regex characters
  464. }
  465. }
  466. /**
  467. * Escape a string to be used in a LDAP filter
  468. *
  469. * Ported from Perl's Net::LDAP::Util escape_filter_value
  470. *
  471. * @param string $string
  472. * @return string
  473. * @author Andreas Gohr
  474. */
  475. protected function filterEscape($string)
  476. {
  477. // see https://github.com/adldap/adLDAP/issues/22
  478. return preg_replace_callback(
  479. '/([\x00-\x1F\*\(\)\\\\])/',
  480. static fn($matches) => "\\" . implode("", unpack("H2", $matches[1])),
  481. $string
  482. );
  483. }
  484. /**
  485. * Opens a connection to the configured LDAP server and sets the wanted
  486. * option on the connection
  487. *
  488. * @author Andreas Gohr <andi@splitbrain.org>
  489. */
  490. protected function openLDAP()
  491. {
  492. if ($this->con) return true; // connection already established
  493. if ($this->getConf('debug')) {
  494. ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7);
  495. }
  496. $this->bound = 0;
  497. $port = $this->getConf('port');
  498. $bound = false;
  499. $servers = explode(',', $this->getConf('server'));
  500. foreach ($servers as $server) {
  501. $server = trim($server);
  502. $this->con = @ldap_connect($server, $port);
  503. if (!$this->con) {
  504. continue;
  505. }
  506. /*
  507. * When OpenLDAP 2.x.x is used, ldap_connect() will always return a resource as it does
  508. * not actually connect but just initializes the connecting parameters. The actual
  509. * connect happens with the next calls to ldap_* funcs, usually with ldap_bind().
  510. *
  511. * So we should try to bind to server in order to check its availability.
  512. */
  513. //set protocol version and dependend options
  514. if ($this->getConf('version')) {
  515. if (
  516. !@ldap_set_option(
  517. $this->con,
  518. LDAP_OPT_PROTOCOL_VERSION,
  519. $this->getConf('version')
  520. )
  521. ) {
  522. msg('Setting LDAP Protocol version ' . $this->getConf('version') . ' failed', -1);
  523. $this->debug('LDAP version set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  524. } else {
  525. //use TLS (needs version 3)
  526. if ($this->getConf('starttls')) {
  527. if (!@ldap_start_tls($this->con)) {
  528. msg('Starting TLS failed', -1);
  529. $this->debug('LDAP TLS set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  530. }
  531. }
  532. // needs version 3
  533. if ($this->getConf('referrals') > -1) {
  534. if (
  535. !@ldap_set_option(
  536. $this->con,
  537. LDAP_OPT_REFERRALS,
  538. $this->getConf('referrals')
  539. )
  540. ) {
  541. msg('Setting LDAP referrals failed', -1);
  542. $this->debug('LDAP referal set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  543. }
  544. }
  545. }
  546. }
  547. //set deref mode
  548. if ($this->getConf('deref')) {
  549. if (!@ldap_set_option($this->con, LDAP_OPT_DEREF, $this->getConf('deref'))) {
  550. msg('Setting LDAP Deref mode ' . $this->getConf('deref') . ' failed', -1);
  551. $this->debug('LDAP deref set: ' . hsc(ldap_error($this->con)), 0, __LINE__, __FILE__);
  552. }
  553. }
  554. /* As of PHP 5.3.0 we can set timeout to speedup skipping of invalid servers */
  555. if (defined('LDAP_OPT_NETWORK_TIMEOUT')) {
  556. ldap_set_option($this->con, LDAP_OPT_NETWORK_TIMEOUT, 1);
  557. }
  558. if ($this->getConf('binddn') && $this->getConf('bindpw')) {
  559. $bound = @ldap_bind($this->con, $this->getConf('binddn'), conf_decodeString($this->getConf('bindpw')));
  560. $this->bound = 2;
  561. } else {
  562. $bound = @ldap_bind($this->con);
  563. }
  564. if ($bound) {
  565. break;
  566. }
  567. }
  568. if (!$bound) {
  569. msg("LDAP: couldn't connect to LDAP server", -1);
  570. $this->debug(ldap_error($this->con), 0, __LINE__, __FILE__);
  571. return false;
  572. }
  573. $this->cando['getUsers'] = true;
  574. return true;
  575. }
  576. /**
  577. * Wraps around ldap_search, ldap_list or ldap_read depending on $scope
  578. *
  579. * @param resource $link_identifier
  580. * @param string $base_dn
  581. * @param string $filter
  582. * @param string $scope can be 'base', 'one' or 'sub'
  583. * @param null|array $attributes
  584. * @param int $attrsonly
  585. * @param int $sizelimit
  586. * @return resource
  587. * @author Andreas Gohr <andi@splitbrain.org>
  588. */
  589. protected function ldapSearch(
  590. $link_identifier,
  591. $base_dn,
  592. $filter,
  593. $scope = 'sub',
  594. $attributes = null,
  595. $attrsonly = 0,
  596. $sizelimit = 0
  597. ) {
  598. if (is_null($attributes)) $attributes = [];
  599. if ($scope == 'base') {
  600. return @ldap_read(
  601. $link_identifier,
  602. $base_dn,
  603. $filter,
  604. $attributes,
  605. $attrsonly,
  606. $sizelimit
  607. );
  608. } elseif ($scope == 'one') {
  609. return @ldap_list(
  610. $link_identifier,
  611. $base_dn,
  612. $filter,
  613. $attributes,
  614. $attrsonly,
  615. $sizelimit
  616. );
  617. } else {
  618. return @ldap_search(
  619. $link_identifier,
  620. $base_dn,
  621. $filter,
  622. $attributes,
  623. $attrsonly,
  624. $sizelimit
  625. );
  626. }
  627. }
  628. /**
  629. * Wrapper around msg() but outputs only when debug is enabled
  630. *
  631. * @param string $message
  632. * @param int $err
  633. * @param int $line
  634. * @param string $file
  635. * @return void
  636. */
  637. protected function debug($message, $err, $line, $file)
  638. {
  639. if (!$this->getConf('debug')) return;
  640. msg($message, $err, $line, $file);
  641. }
  642. }