1<?php
2
3/**
4 * DokuWiki Plugin authshibboleth (Auth Component).
5 *
6 * @author  Ivan Novakov http://novakov.cz/
7 * @license http://debug.cz/license/bsd-3-clause BSD 3 Clause
8 * @link https://github.com/ivan-novakov/dokuwiki-shibboleth-auth
9 */
10
11// must be run within Dokuwiki
12if (! defined('DOKU_INC'))
13    die();
14
15
16class auth_plugin_authshibboleth extends DokuWiki_Auth_Plugin
17{
18
19    const CONF_VAR_REMOTE_USER = 'var_remote_user';
20
21    const CONF_VAR_DISPLAY_NAME = 'var_display_name';
22
23    const CONF_VAR_MAIL = 'var_mail';
24
25    const CONF_VAR_SHIB_SESSION_ID = 'var_shib_session_id';
26
27    const CONF_LOGOUT_HANDLER = 'logout_handler';
28
29    const CONF_LOGOUT_HANDLER_LOCATION = 'logout_handler_location';
30
31    const CONF_LOGOUT_RETURN_URL = 'logout_return_url';
32
33    const CONF_SHIBBOLETH_HANDLER_BASE = 'shibboleth_handler_base';
34
35    const CONF_DISPLAY_NAME_TPL = 'display_name_tpl';
36
37    const CONF_USE_DOKUWIKI_SESSION = 'use_dokuwiki_session';
38
39    const CONF_GROUP_SOURCE_CONFIG = 'group_source_config';
40
41    const CONF_LOG_ENABLED = 'log_enabled';
42
43    const CONF_LOG_FILE = 'log_file';
44
45    const CONF_LOG_TO_PHP = 'log_to_php';
46
47    const CONF_LOG_PRIORITY = 'log_priority';
48
49    const CONF_AUTH_USERSFILE = 'auth_usersfile';
50
51    const USER_UID = 'uid';
52
53    const USER_NAME = 'name';
54
55    const USER_MAIL = 'mail';
56
57    const USER_GRPS = 'grps';
58
59    const GROUP_SOURCE_TYPE_ENVIRONMENT = 'environment';
60
61    const GROUP_SOURCE_TYPE_FILE = 'file';
62
63    const LOG_DEBUG = 7;
64
65    const LOG_INFO = 6;
66
67    const LOG_ERR = 3;
68
69    /**
70     * Global configuration.
71     * @var array
72     */
73    protected $globalConf = array();
74
75    /**
76     * Environment variable values ($_SERVER).
77     * @var array
78     */
79    protected $environment = array();
80
81    /**
82     * User information as gathered from the environment.
83     * @var array
84     */
85    protected $userInfo = array();
86
87
88    /**
89     * Constructor.
90     */
91    public function __construct()
92    {
93        $this->cando['external'] = true;
94        $this->cando['logoff'] = true;
95
96        if(@is_writable(DOKU_CONF . '/' . $this->getConf(self::CONF_AUTH_USERSFILE))) {
97            $this->cando['getUsers'] = true;
98            $this->cando['getUserCount'] = true;
99        }
100
101        $this->setEnvironment($_SERVER);
102
103        global $conf;
104        $this->setGlobalConfiguration($conf);
105
106        $this->success = true;
107    }
108
109
110   /**
111     * Load all user data
112     *
113     * loads the user file into a datastructure
114     *
115     * @author  Andreas Gohr <andi@splitbrain.org>
116     */
117    protected function _loadUserData() {
118        global $config_cascade;
119        $this->users = array();
120        if(!file_exists(DOKU_CONF . '/' . $this->getConf(self::CONF_AUTH_USERSFILE))) return;
121
122        $lines = file(DOKU_CONF . '/' . $this->getConf(self::CONF_AUTH_USERSFILE));
123        foreach($lines as $line) {
124            $line = preg_replace('/#.*$/', '', $line); //ignore comments
125            $line = trim($line);
126            if(empty($line)) continue;
127
128            /* NB: preg_split can be deprecated/replaced with str_getcsv once dokuwiki is min php 5.3 */
129            $row = $this->_splitUserData($line);
130            $row = str_replace('\\:', ':', $row);
131            $row = str_replace('\\\\', '\\', $row);
132            $groups = array_values(array_filter(explode(",", $row[3])));
133            $this->users[$row[0]]['name'] = urldecode($row[1]);
134            $this->users[$row[0]]['mail'] = $row[2];
135            $this->users[$row[0]]['grps'] = $groups;
136        }
137    }
138
139    protected function _splitUserData($line){
140        // due to a bug in PCRE 6.6, preg_split will fail with the regex we use here
141        // refer github issues 877 & 885
142        if ($this->_pregsplit_safe){
143            return preg_split('/(?<![^\\\\]\\\\)\:/', $line, 5);       // allow for : escaped as \:
144        }
145        $row = array();
146        $piece = '';
147        $len = strlen($line);
148        for($i=0; $i<$len; $i++){
149            if ($line[$i]=='\\'){
150                $piece .= $line[$i];
151                $i++;
152                if ($i>=$len) break;
153            } else if ($line[$i]==':'){
154                $row[] = $piece;
155                $piece = '';
156                continue;
157            }
158            $piece .= $line[$i];
159        }
160        $row[] = $piece;
161        return $row;
162    }
163
164    /**
165     * construct a filter pattern
166     *
167     * @param array $filter
168     */
169    protected function _constructPattern($filter) {
170        $this->_pattern = array();
171        foreach($filter as $item => $pattern) {
172            $this->_pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters
173        }
174    }
175
176    /**
177     * return true if $user + $info match $filter criteria, false otherwise
178     *
179     * @author   Chris Smith <chris@jalakai.co.uk>
180     *
181     * @param string $user User login
182     * @param array  $info User's userinfo array
183     * @return bool
184     */
185    protected function _filter($user, $info) {
186        foreach($this->_pattern as $item => $pattern) {
187            if($item == 'user') {
188                if(!preg_match($pattern, $user)) return false;
189            } else if($item == 'grps') {
190                if(!count(preg_grep($pattern, $info['grps']))) return false;
191            } else {
192                if(!preg_match($pattern, $info[$item])) return false;
193            }
194        }
195        return true;
196    }
197
198    /**
199     * Sets the environment variables.
200     *
201     * @param array $environment
202     */
203    public function setEnvironment(array $environment)
204    {
205        $this->environment = $environment;
206    }
207
208
209    /**
210     * Sets the global configuration variables.
211     *
212     * @param array $globalConf
213     */
214    public function setGlobalConfiguration(array $globalConf)
215    {
216        $this->globalConf = $globalConf;
217    }
218
219    /**
220     * Return user info
221     *
222     * Returns info about the given user needs to contain
223     * at least these fields:
224     *
225     * name string  full name of the user
226     * mail string  email addres of the user
227     * grps array   list of groups the user is in
228     *
229     * @author  Andreas Gohr <andi@splitbrain.org>
230     * @param string $user
231     * @param bool $requireGroups  (optional) ignored by this plugin, grps info always supplied
232     * @return array|false
233     */
234    public function getUserData($user, $requireGroups=true) {
235        if($this->users === null) $this->_loadUserData();
236        return isset($this->users[$user]) ? $this->users[$user] : false;
237    }
238
239   /**
240     * Return a count of the number of user which meet $filter criteria
241     *
242     * @author  Chris Smith <chris@jalakai.co.uk>
243     *
244     * @param array $filter
245     * @return int
246     */
247    public function getUserCount($filter = array()) {
248        if($this->users === null) $this->_loadUserData();
249        if(!count($filter)) return count($this->users);
250        $count = 0;
251        $this->_constructPattern($filter);
252        foreach($this->users as $user => $info) {
253            $count += $this->_filter($user, $info);
254        }
255        return $count;
256    }
257
258    /**
259     * Bulk retrieval of user data
260     *
261     * @author  Chris Smith <chris@jalakai.co.uk>
262     *
263     * @param   int   $start index of first user to be returned
264     * @param   int   $limit max number of users to be returned
265     * @param   array $filter array of field/pattern pairs
266     * @return  array userinfo (refer getUserData for internal userinfo details)
267     */
268    public function retrieveUsers($start = 0, $limit = 0, $filter = array()) {
269        if($this->users === null) $this->_loadUserData();
270        ksort($this->users);
271        $i     = 0;
272        $count = 0;
273        $out   = array();
274        $this->_constructPattern($filter);
275        foreach($this->users as $user => $info) {
276            if($this->_filter($user, $info)) {
277                if($i >= $start) {
278                    $out[$user] = $info;
279                    $count++;
280                    if(($limit > 0) && ($count >= $limit)) break;
281                }
282                $i++;
283            }
284        }
285        return $out;
286    }
287
288    /**
289     * {@inheritdoc}
290     * @see DokuWiki_Auth_Plugin::trustExternal()
291     */
292    public function trustExternal()
293    {
294        $this->debug('Checking for DokuWiki session...');
295        if ($this->getConf(self::CONF_USE_DOKUWIKI_SESSION) && ($userInfo = $this->loadUserInfoFromSession()) !== null) {
296            $this->log('Loaded user from DokuWiki session');
297            return;
298        }
299
300        $sessionVarName = $this->getConf(self::CONF_VAR_SHIB_SESSION_ID);
301        $this->debug(sprintf("Checking for Shibboleth session [%s] ...", $sessionVarName));
302        if ($this->getShibVar($sessionVarName)) {
303            $this->log('Shibboleth session found, trying to authenticate user...');
304
305            $userId = $this->getShibVar($this->getConf(self::CONF_VAR_REMOTE_USER));
306            if ($userId) {
307
308                $this->setUserId($userId);
309                $this->setUserDisplayName($this->retrieveUserDisplayName());
310                $this->setUserMail($this->retrieveUserMail());
311                $this->setUserGroups($this->retrieveUserGroups());
312
313                $this->saveUserInfoToSession();
314                $this->saveGlobalUserInfo();
315
316                $this->_saveUserInfo();
317
318                $this->log('Loaded user from environment');
319
320                return true;
321            }
322        }
323
324        auth_logoff();
325        return false;
326    }
327
328
329    /**
330     * {@inheritdoc}
331     * @see DokuWiki_Auth_Plugin::logOff()
332     */
333    public function logOff()
334    {
335        /*
336         * Initiate a logout sequence only, if there is a Shibboleth identity
337         */
338        if ($this->retrieveUserId()) {
339            $url = $this->getConf(self::CONF_LOGOUT_HANDLER_LOCATION);
340            if (! $url) {
341                $url = $this->createLogoutHandlerLocation();
342            }
343
344            $this->debug(sprintf("Logout redirect: %s", $url));
345
346            header('Location: ' . $url);
347            exit();
348        }
349    }
350
351
352    /**
353     * Saves user info into the session.
354     */
355    protected function saveUserInfoToSession(array $userInfo = null)
356    {
357        if (! $userInfo) {
358            $userInfo = $this->getUserInfo();
359        }
360
361        $_SESSION[DOKU_COOKIE]['auth']['user'] = $userInfo['uid'];
362        $_SESSION[DOKU_COOKIE]['auth']['info'] = $userInfo;
363
364
365    }
366
367
368    /**
369     * Loads user info from the session.
370     *
371     * @return array|null
372     */
373    protected function loadUserInfoFromSession()
374    {
375        if (isset($_SESSION[DOKU_COOKIE]['auth']) && is_array($_SESSION[DOKU_COOKIE]['auth'])) {
376            $authInfo = $_SESSION[DOKU_COOKIE]['auth'];
377
378            if (isset($authInfo['user']) && isset($authInfo['info']) && is_array($authInfo['info'])) {
379                $userInfo = $authInfo['info'];
380                $username = $authInfo['user'];
381
382                $this->setUserInfo($userInfo);
383                $this->saveGlobalUserInfo();
384
385                return $userInfo;
386            }
387        }
388
389        return null;
390    }
391
392    protected function _saveUserInfo() {
393        $userInfo = $this->getUserInfo();
394
395        // user mustn't already exist
396        if ($this->getUserData($userInfo['uid']) === false) {
397            // prepare user line
398            $groups = join(',',$userInfo['grps']);
399            $userline = join(':',array($userInfo['uid'], $userInfo['name'], $userInfo['mail'], $groups))."\n";
400
401            if (io_saveFile(DOKU_CONF . '/' . $this->getConf(self::CONF_AUTH_USERSFILE), $userline, true)) {
402                $this->users[$userInfo['uid']] = compact('name', 'mail', 'grps');
403            } else {
404                msg('The ' . DOKU_CONF . '/' . $this->getConf(self::CONF_AUTH_USERSFILE) . ' file is not writable. Please inform the Wiki-Admin',-1);
405            }
406        }
407    }
408
409    /**
410     * Sets user info accordingly to the DokuWiki speifics.
411     *
412     * Sets the $USERINFO global variable. Sets the REMOTE_USER variable, if it is not populated with the
413     * username from the Shibboleth environment. Despite having the $USERINFO global array, it seems that
414     * DokuWiki still uses the REMOTE_USER value.
415     *
416     * @param array $userInfo
417     */
418    protected function saveGlobalUserInfo(array $userInfo = null)
419    {
420        global $USERINFO;
421
422        if (! $userInfo) {
423            $userInfo = $this->getUserInfo();
424        }
425
426        $USERINFO = $userInfo;
427        $_SERVER['REMOTE_USER'] = $userInfo['uid'];
428    }
429
430
431    /**
432     * Returns the value of global configuration variable.
433     *
434     * @param string $varName
435     * @return mixed|null
436     */
437    protected function getGlobalConfVar($varName)
438    {
439        if (isset($this->globalConf[$varName])) {
440            return $this->globalConf[$varName];
441        }
442
443        return null;
444    }
445
446
447    /**
448     * Returns a Shibboleth variable.
449     *
450     * @param string $varName
451     * @param boolean $multivalue
452     * @return string|array|null
453     */
454    protected function getShibVar($varName, $multivalue = false)
455    {
456        $value = $this->getEnvVar($varName);
457        if ($value && $multivalue) {
458            $value = explode(';', $value);
459        }
460
461        return $value;
462    }
463
464
465    /**
466     * Returns the value of the required environment variable.
467     *
468     * @param string $varName
469     * @return string|null
470     */
471    protected function getEnvVar($varName)
472    {
473        if (isset($this->environment[$varName])) {
474            return $this->environment[$varName];
475        }
476
477        return null;
478    }
479
480
481    /**
482     * Extracts user's identity from the environment.
483     *
484     * @return string|null
485     */
486    protected function retrieveUserId()
487    {
488        return $this->getShibVar($this->getConf(self::CONF_VAR_REMOTE_USER));
489    }
490
491
492    /**
493     * Extracts user's mail from the environment.
494     *
495     * @return string|null
496     */
497    protected function retrieveUserMail()
498    {
499        $mails = $this->getShibVar($this->getConf(self::CONF_VAR_MAIL), true);
500        if (count($mails)) {
501            return $mails[0];
502        }
503
504        return null;
505    }
506
507
508    /**
509     * Extracts user's display name from the environment.
510     *
511     * @return string|null
512     */
513    protected function retrieveUserDisplayName()
514    {
515        $userDisplayName = null;
516        $tplUserDisplayName = $this->getConf(self::CONF_DISPLAY_NAME_TPL);
517        $userDisplayNameVar = $this->getConf(self::CONF_VAR_DISPLAY_NAME);
518
519        if ($tplUserDisplayName) {
520            $userDisplayName = $this->retrieveUserDisplayNameFromTpl($tplUserDisplayName);
521        }
522        else if ($userDisplayNameVar) {
523            $userDisplayName = $this->getShibVar($userDisplayNameVar);
524        }
525
526        if (! $userDisplayName) {
527            $userDisplayName = $this->getUserId();
528        }
529
530        return $userDisplayName;
531    }
532
533
534    /**
535     * Resolves the template for the user's real name and returns it.
536     *
537     * @param string $tplUserDisplayName
538     * @return string
539     */
540    protected function retrieveUserDisplayNameFromTpl($tplUserDisplayName)
541    {
542        $matches = array();
543        if (preg_match_all('/({([^{}]+)})/', $tplUserDisplayName, $matches)) {
544            $vars = $matches[2];
545
546            $userName = $tplUserDisplayName;
547            foreach ($vars as $var) {
548                $value = $this->getShibVar($var);
549                if (! $value) {
550                    return '';
551                }
552                $userName = str_replace('{' . $var . '}', $value, $userName);
553            }
554
555            return $userName;
556        }
557
558        return '';
559    }
560
561
562    /**
563     * Resolves the user's groups.
564     *
565     * @return array
566     */
567    protected function retrieveUserGroups()
568    {
569        $groups = array();
570
571        // default groups
572        if (($defaultGroup = $this->getGlobalConfVar('defaultgroup')) !== null) {
573            $groups[] = $defaultGroup;
574        }
575
576        $groupSourceConfig = $this->getConf(self::CONF_GROUP_SOURCE_CONFIG);
577        if (is_array($groupSourceConfig)) {
578            foreach ($groupSourceConfig as $sourceName => $config) {
579                if (! isset($config['type'])) {
580                    $this->log(sprintf("Group source '%s' without a type", $sourceName));
581                    continue;
582                }
583                $sourceType = $config['type'];
584                $sourceOptions = array();
585                if (isset($config['options'])) {
586                    $sourceOptions = $config['options'];
587                }
588
589                $sourceGroups = $this->retrieveUserGroupsFromSource($sourceName, $sourceType, $sourceOptions);
590                if (is_array($sourceGroups)) {
591                    $groups = array_merge($groups, $sourceGroups);
592                }
593            }
594        } else {
595            $this->log(sprintf("The value of '%s' must be an array", self::CONF_GROUP_SOURCE_CONFIG));
596        }
597
598        $this->log(sprintf("Resolved groups: %s", implode(', ', $groups)));
599
600        return $groups;
601    }
602
603
604    /**
605     * Resolves the user's groups from different sources.
606     *
607     * @param string $sourceType
608     * @param array $sourceOptions
609     * @return array
610     */
611    protected function retrieveUserGroupsFromSource($sourceName, $sourceType, array $sourceOptions)
612    {
613        $groups = array();
614
615        $this->debug(sprintf("Resolving groups from source '%s' (%s)", $sourceName, $sourceType));
616
617        $handler = 'retrieveUserGroupsFrom' . ucfirst($sourceType);
618        if (! method_exists($this, $handler)) {
619            $this->log(sprintf("Non-existent group source handler '%s'", $handler));
620            return $groups;
621        }
622
623        try {
624            $sourceGroups = call_user_func_array(array(
625                $this,
626                $handler
627            ), array(
628                $sourceOptions
629            ));
630        } catch (Exception $e) {
631            $this->log(sprintf("Error retrieving groups from source '%s' (%s): %s", $sourceName, $sourceType, $e->getMessage()));
632            return $groups;
633        }
634
635        $this->debug(sprintf("Resolved groups from source '%s' (%s): %s", $sourceName, $sourceType, implode(', ', $sourceGroups)));
636
637        /*
638         * Groups "post-processing"
639         */
640        foreach ($sourceGroups as $group) {
641            if (isset($sourceOptions['map'])) {
642                $map = $sourceOptions['map'];
643                if (isset($map[$group])) {
644                    $group = $map[$group];
645                }
646            }
647
648            if (isset($sourceOptions['prefix'])) {
649                $group = $sourceOptions['prefix'] . $group;
650            }
651
652            $groups[] = $group;
653        }
654
655        return $groups;
656    }
657
658
659    /**
660     * Resolves user's groups from the environment variables.
661     *
662     * @param array $options
663     * @throws RuntimeException
664     * @return array
665     */
666    protected function retrieveUserGroupsFromEnvironment(array $options)
667    {
668        $groups = array();
669
670        if (! isset($options['source_attribute'])) {
671            throw new RuntimeException('The required "source_attribute" option not set');
672        }
673        $sourceAttributeName = $options['source_attribute'];
674
675        $values = $this->getShibVar($sourceAttributeName, true);
676        if (null !== $values) {
677            foreach ($values as $value) {
678
679                $groups[] = $value;
680            }
681        }
682
683        return $groups;
684    }
685
686
687    /**
688     * Resolves user's groups from a file.
689     *
690     * @param array $options
691     * @throws RuntimeException
692     * @return array
693     */
694    protected function retrieveUserGroupsFromFile(array $options)
695    {
696        $groups = array();
697
698        $userId = $this->getUserId();
699        if (! $userId) {
700            throw new RuntimeException('No user identity');
701        }
702
703        if (! isset($options['path'])) {
704            throw new RuntimeException('The required "path" option not set');
705        }
706
707        $path = $options['path'];
708        if (! file_exists($path)) {
709            throw new RuntimeException(sprintf("Non-existent file '%s'", $path));
710        }
711
712        if (! is_readable($path)) {
713            throw new RuntimeException(sprintf("File '%s' not readable", $path));
714        }
715
716        $sourceGroups = require $path;
717        if (! is_array($sourceGroups)) {
718            throw new RuntimeException(sprintf("Invalid group format in file '%s'", $path));
719        }
720
721        foreach ($sourceGroups as $groupName => $members) {
722            if (in_array($userId, $members)) {
723                $groups[] = $groupName;
724            }
725        }
726
727        return $groups;
728    }
729
730
731    /**
732     * Returns the user ID (user's identity value).
733     *
734     * @return string|null
735     */
736    protected function getUserId()
737    {
738        return $this->getUserVar(self::USER_UID);
739    }
740
741
742    /**
743     * Sets the user ID.
744     *
745     * @param string $userId
746     */
747    protected function setUserId($userId)
748    {
749        $this->setUserVar(self::USER_UID, $userId);
750    }
751
752
753    /**
754     * Returns the user's display name.
755     *
756     * @return string|null
757     */
758    protected function getUserDisplayName()
759    {
760        return $this->getUserVar(self::USER_NAME);
761    }
762
763
764    /**
765     * Sets the user's display name.
766     *
767     * @param string $userDisplayName
768     */
769    protected function setUserDisplayName($userDisplayName)
770    {
771        $this->setUserVar(self::USER_NAME, $userDisplayName);
772    }
773
774
775    /**
776     * Returns the user's mail.
777     *
778     * @return string|null
779     */
780    protected function getUserMail()
781    {
782        return $this->getUserVar(self::USER_MAIL);
783    }
784
785
786    /**
787     * Sets the user's mail.
788     *
789     * @param string $mail
790     */
791    protected function setUserMail($mail)
792    {
793        $this->setUserVar(self::USER_MAIL, $mail);
794    }
795
796
797    /**
798     * Returns the list of user's groups.
799     *
800     * @return array
801     */
802    protected function getUserGroups()
803    {
804        return $this->getUserVar(self::USER_GRPS);
805    }
806
807
808    /**
809     * Sets the user's groups.
810     *
811     * @param array $groups
812     */
813    protected function setUserGroups(array $groups)
814    {
815        $this->setUserVar(self::USER_GRPS, $groups);
816    }
817
818
819    /**
820     * Sets a specific user variable value.
821     *
822     * @param string $varName
823     * @param mixed $varValue
824     */
825    protected function setUserVar($varName, $varValue)
826    {
827        $this->userInfo[$varName] = $varValue;
828    }
829
830
831    /**
832     * Returns a specific user variable value.
833     *
834     * @param string $varName
835     * @return mixed|null
836     */
837    protected function getUserVar($varName)
838    {
839        if (isset($this->userInfo[$varName])) {
840            return $this->userInfo[$varName];
841        }
842
843        return null;
844    }
845
846
847    /**
848     * Sets all the user info at once.
849     *
850     * @param array $userInfo
851     */
852    protected function setUserInfo(array $userInfo)
853    {
854        $this->userInfo = $userInfo;
855    }
856
857
858    /**
859     * Returns all the user info.
860     *
861     * @return array
862     */
863    protected function getUserInfo()
864    {
865        return $this->userInfo;
866    }
867
868
869    /**
870     * Build a logout handler URL.
871     *
872     * @param string $returnUrl
873     * @param string $handlerName
874     * @return string
875     */
876    protected function createLogoutHandlerLocation($returnUrl = NULL, $handlerName = 'Logout')
877    {
878        if (! $returnUrl) {
879            if (isset($_SERVER['HTTP_REFERER']) && isset($_SERVER['HTTP_REFERER'])) {
880                $returnUrl = $_SERVER['HTTP_REFERER'];
881            } else {
882                $returnUrl = $this->getConf(self::CONF_LOGOUT_RETURN_URL);
883            }
884        }
885
886        if (! $handlerName) {
887            $handlerName = $this->getConf(self::CONF_LOGOUT_HANDLER);
888        }
889
890        return sprintf("https://%s%s%s?return=%s", $_SERVER['HTTP_HOST'], $this->getConf(self::CONF_SHIBBOLETH_HANDLER_BASE), $handlerName, $returnUrl);
891    }
892
893
894    /**
895     * Logs a debug message.
896     *
897     * @param string $message
898     */
899    protected function debug($message)
900    {
901        $this->log($message, self::LOG_DEBUG);
902    }
903
904
905    /**
906     * Logs an error message.
907     *
908     * @param string $message
909     */
910    protected function err($message)
911    {
912        $this->log($message, self::LOG_ERR);
913    }
914
915
916    /**
917     * Log a message.
918     *
919     * @param mixed $message
920     * @param integer $priority
921     */
922    protected function log($message, $priority = self::LOG_INFO)
923    {
924        $message = $this->logFormatMessage($message, $priority);
925
926        if ($this->getConf(self::CONF_LOG_ENABLED) && $priority <= $this->getConf(self::CONF_LOG_PRIORITY)) {
927            if ($this->getConf(self::CONF_LOG_TO_PHP)) {
928                error_log($message);
929            }
930
931            $logFile = $this->getConf(self::CONF_LOG_FILE);
932            if ($logFile) {
933                $flags = null;
934                if (file_exists($logFile)) {
935                    if (! is_writable($logFile)) {
936                        $this->debug(sprintf("Log file '%s' not writable", $logFile));
937                        return;
938                    }
939                    $flags = FILE_APPEND;
940                }
941
942                $message = sprintf("[%s]: %s\n", date('c', time()), $message);
943                if (false === file_put_contents($logFile, $message, $flags)) {
944                    $this->debug(sprintf("Error writing to log file '%s'", $logFile));
945                }
946            } elseif (! $this->getConf(self::CONF_LOG_TO_PHP)) {
947                $this->debug('Log enabled, but log file not set');
948            }
949        }
950    }
951
952
953    /**
954     * Formats a log message.
955     *
956     * @param mixed $message
957     * @param integer $priority
958     * @return string
959     */
960    protected function logFormatMessage($message, $priority)
961    {
962        if (! is_scalar($message)) {
963            $message = print_r($message, true);
964        }
965
966        $userId = $this->getUserId();
967        if (! $userId) {
968            $userId = 'unknown';
969        }
970        return sprintf("(%d) [%s/%s] %s [%s]", $priority, $userId, $_SERVER['REMOTE_ADDR'], $message, $_SERVER['REQUEST_URI']);
971    }
972}
973