1<?php
2/**
3 * DokuWiki Plugin for Magento (Auth Component)
4 *
5 * See the configuration. There are settings for customers, groups, administrators, and roles.
6 *
7 * Notes:
8 * - Magento customer data is retrieved only when necessary, and cached in memory.
9 * - Magento administrator data is retrieved only when necessary, and cached in memory.
10 * - Password hashes are retrieved, but never stored in memory.
11 *
12 * @license GPL v3 http://www.gnu.org
13 * @author  Z3 Development <z3-dev@gfnews.net>
14 */
15
16// must be run within Dokuwiki
17if(!defined('DOKU_INC')) die();
18
19class auth_plugin_magento extends DokuWiki_Auth_Plugin {
20    /** @var array user data for indentifying logins */
21    protected $users  = null;
22    /** @var array administrator data for indentifying logins */
23    protected $admins = null;
24    /** @var array roles for provisioning administrators */
25    protected $roles  = null;
26
27    protected $db_dsn    = null;
28    protected $db_user   = null;
29    protected $db_passwd = null;
30
31    /**
32     * Constructor.
33     */
34    public function __construct() {
35        parent::__construct(); // for compatibility
36
37        $this->cando['addUser']     = false; // can Users be created?
38        $this->cando['delUser']     = false; // can Users be deleted?
39        $this->cando['modLogin']    = false; // can login names be changed?
40        $this->cando['modPass']     = false; // can passwords be changed?
41        $this->cando['modName']     = false; // can real names be changed?
42        $this->cando['modMail']     = false; // can emails be changed?
43        $this->cando['modGroups']   = false; // can groups be changed?
44        $this->cando['getUsers']    = false; // can a (filtered) list of users be retrieved?
45        $this->cando['getUserCount']= false; // can the number of users be retrieved?
46        $this->cando['getGroups']   = false; // can a list of available groups be retrieved?
47        $this->cando['external']    = false; // does the module do external auth checking?
48        $this->cando['logout']      = true;  // can the user logout again? (eg. not possible with HTTP auth)
49
50
51        $this->db_dsn    = $this->getConf( 'databaseDSN' );
52        $this->db_user   = $this->getConf( 'databaseUser' );
53        $this->db_passwd = $this->getConf( 'databasePassword' );
54
55        // set success to true, and let DokuWiki take over
56        $this->success = true;
57    }
58
59
60    /**
61     * Log off the current user [ OPTIONAL ]
62     */
63    //public function logOff() {
64    //}
65
66    /**
67     * Do all authentication [ OPTIONAL ]
68     *
69     * @param   string  $user    Username
70     * @param   string  $pass    Cleartext Password
71     * @param   bool    $sticky  Cookie should not expire
72     * @return  bool             true on successful auth
73     */
74    //public function trustExternal($user, $pass, $sticky = false) {
75    //}
76
77    /**
78     * Check user+password
79     *
80     * May be ommited if trustExternal is used.
81     *
82     * @param   string $user the user name
83     * @param   string $pass the clear text password
84     * @return  bool true if verified
85     */
86    public function checkPass($user, $pass) {
87        $entity = $this->_findUser( $user );
88        if ( $entity > 0 ) {
89            return $this->_checkUserPassword( $entity, $pass );
90        }
91        $entity = $this->_findAdmin( $user );
92        if ( $entity > 0 ) {
93            return $this->_checkAdminPassword( $entity, $pass );
94        }
95        return false;
96    }
97
98    /**
99     * Return user info
100     *
101     * Returns info about the given user needs to contain
102     * at least these fields:
103     *
104     * name string  full name of the user
105     * mail string  email address of the user
106     * grps array   list of groups the user is in
107     *
108     * @param   string $user the user name
109     * @return  array containing user data or false
110     */
111    public function getUserData($user) {
112        global $conf;
113
114        $entity = $this->_findUser( $user );
115        if ( $entity > 0 ) {
116            $name = "{$this->users[$entity]['first']} {$this->users[$entity]['last']}";
117
118            if ( ! isset( $this->users[$entity]['mail']) ) $this->_loadMailAddress( $entity );
119
120            if ( ! isset( $this->users[$entity]['groups']) ) $this->_loadUserGroups( $entity );
121
122        // add magento groups
123            $groups = $this->users[$entity]['groups'];
124        // add default group (if configured)
125            if ( $this->getConf( 'includeDefaultGroup' ) == 1 ) {
126                array_push( $groups, $conf[ 'defaultgroup' ] );
127            }
128        // add additional groups (if configured)
129            if ( $this->getConf( 'userGroups' ) ) {
130                $usergroups = array_values( array_filter( explode( ",", $this->getConf( 'userGroups' ) ) ) );
131                $groups = array_merge( $groups, $usergroups );
132            }
133
134            $data = array();
135            $data['name'] = $name;
136            $data['mail'] = $this->users[$entity]['mail'];
137            $data['grps'] = $groups;
138            return $data;
139        }
140        $entity = $this->_findAdmin( $user );
141        if ( $entity > 0 ) {
142            if ( ! isset( $this->admins[$entity]['roles']) ) $this->_loadAdminRoles( $entity );
143
144        // add magento roles
145            $groups = $this->admins[$entity]['roles'];
146        // add default group (if configured)
147            if ( $this->getConf( 'includeDefaultGroup' ) == 1 ) {
148                array_push( $groups, $conf[ 'defaultgroup' ] );
149            }
150        // add additional groups (if configured)
151            if ( $this->getConf( 'adminGroups' ) ) {
152                $usergroups = array_values( array_filter( explode( ",", $this->getConf( 'adminGroups' ) ) ) );
153                $groups = array_merge( $groups, $usergroups );
154            }
155
156            $data = array();
157            $data['name'] = "{$this->admins[$entity]['first']} {$this->admins[$entity]['last']}";
158            $data['mail'] = $this->admins[$entity]['mail'];
159            $data['grps'] = $groups;
160            return $data;
161        }
162        return false;
163    }
164
165    /**
166     * Create a new User [implement only where required/possible]
167     *
168     * Returns false if the user already exists, null when an error
169     * occurred and true if everything went well.
170     *
171     * The new user HAS TO be added to the default group by this
172     * function!
173     *
174     * Set addUser capability when implemented
175     *
176     * @param  string     $user
177     * @param  string     $pass
178     * @param  string     $name
179     * @param  string     $mail
180     * @param  null|array $grps
181     * @return bool|null
182     */
183    //public function createUser($user, $pass, $name, $mail, $grps = null) {
184    //}
185
186    /**
187     * Modify user data [implement only where required/possible]
188     *
189     * Set the mod* capabilities according to the implemented features
190     *
191     * @param   string $user    nick of the user to be changed
192     * @param   array  $changes array of field/value pairs to be changed (password will be clear text)
193     * @return  bool
194     */
195    //public function modifyUser($user, $changes) {
196    //}
197
198    /**
199     * Delete one or more users [implement only where required/possible]
200     *
201     * Set delUser capability when implemented
202     *
203     * @param   array  $users
204     * @return  int    number of users deleted
205     */
206    //public function deleteUsers($users) {
207    //}
208
209    /**
210     * Bulk retrieval of user data [implement only where required/possible]
211     *
212     * Set getUsers capability when implemented
213     *
214     * @param   int   $start     index of first user to be returned
215     * @param   int   $limit     max number of users to be returned
216     * @param   array $filter    array of field/pattern pairs, null for no filter
217     * @return  array list of userinfo (refer getUserData for internal userinfo details)
218     */
219    //public function retrieveUsers($start = 0, $limit = -1, $filter = null) {
220    //}
221
222    /**
223     * Return a count of the number of user which meet $filter criteria
224     * [should be implemented whenever retrieveUsers is implemented]
225     *
226     * Set getUserCount capability when implemented
227     *
228     * @param  array $filter array of field/pattern pairs, empty array for no filter
229     * @return int
230     */
231    //public function getUserCount($filter = array()) {
232    //}
233
234    /**
235     * Define a group [implement only where required/possible]
236     *
237     * Set addGroup capability when implemented
238     *
239     * @param   string $group
240     * @return  bool
241     */
242    //public function addGroup($group) {
243    //}
244
245    /**
246     * Retrieve groups [implement only where required/possible]
247     *
248     * Set getGroups capability when implemented
249     *
250     * @param   int $start
251     * @param   int $limit
252     * @return  array
253     */
254    //public function retrieveGroups($start = 0, $limit = 0) {
255    //    return array();
256    //}
257
258    /**
259     * Return case sensitivity of the backend
260     *
261     * When your backend is caseinsensitive (eg. you can login with USER and
262     * user) then you need to overwrite this method and return false
263     *
264     * @return bool
265     */
266    public function isCaseSensitive() {
267    // NOTE: Magento administrators login with "user name" which is case sensitive.
268        return true;
269    }
270
271    /**
272     * Sanitize a given username
273     *
274     * This function is applied to any user name that is given to
275     * the backend and should also be applied to any user name within
276     * the backend before returning it somewhere.
277     *
278     * This should be used to enforce username restrictions.
279     *
280     * @param string $user username
281     * @return string the cleaned username
282     */
283    public function cleanUser( $user ) {
284        return cleanID( str_replace( ':', $this->getConf['sepchar'], $user ) );
285    }
286
287    /**
288     * Sanitize a given groupname
289     *
290     * This function is applied to any groupname that is given to
291     * the backend and should also be applied to any groupname within
292     * the backend before returning it somewhere.
293     *
294     * This should be used to enforce groupname restrictions.
295     *
296     * Groupnames are to be passed without a leading '@' here.
297     *
298     * @param  string $group groupname
299     * @return string the cleaned groupname
300     */
301    public function cleanGroup( $group ) {
302        return cleanID( str_replace(':', $this->getConf['sepchar'], $group ) );
303    }
304
305    /**
306     * Check Session Cache validity [implement only where required/possible]
307     *
308     * DokuWiki caches user info in the user's session for the timespan defined
309     * in $conf['auth_security_timeout'].
310     *
311     * This makes sure slow authentication backends do not slow down DokuWiki.
312     * This also means that changes to the user database will not be reflected
313     * on currently logged in users.
314     *
315     * To accommodate for this, the user manager plugin will touch a reference
316     * file whenever a change is submitted. This function compares the filetime
317     * of this reference file with the time stored in the session.
318     *
319     * This reference file mechanism does not reflect changes done directly in
320     * the backend's database through other means than the user manager plugin.
321     *
322     * Fast backends might want to return always false, to force rechecks on
323     * each page load. Others might want to use their own checking here. If
324     * unsure, do not override.
325     *
326     * @param  string $user - The username
327     * @return bool
328     */
329    //public function useSessionCache($user) {
330    //}
331
332/*
333 * Implementation specific functions.
334 */
335    /**
336     * Find user entity from the user data
337     *
338     * Magento (default) requires both first and last names
339     *
340     * The given user is matched against the first and last names of Magento in either order
341     * All spaces in names are converted to the seperator character
342     *
343     * @param   string $user the user name
344     * @return  int          the entity found in the list of users or -1
345     */
346    protected function _findUser( $user ) {
347    // load the user data if not already
348        if( $this->users === null ) $this->_loadUserData();
349
350        $sep = $this->getConf['sepchar'];
351    // find the given user in the user data
352        $count = 0;
353        $entity = 0;
354        foreach( $this->users as $entry ) {
355            $first_last = "{$entry['first']} {$entry['last']}";
356            $first_last = cleanID( $first_last );
357            $last_first = "{$entry['last']} {$entry['first']}";
358            $last_first = cleanID( $last_first );
359            if ( strnatcasecmp ( $user , $first_last ) === 0 ) {
360                $entity = $entry['entity'];
361                $count = $count + 1;
362            }
363            if ( strnatcasecmp ( $user , $last_first ) === 0 ) {
364                $entity = $entry['entity'];
365                $count = $count + 1;
366            }
367        }
368        if ( $count == 1 ) return $entity;
369        if ( $count > 1 ) msg( "Your user name is ambiguous. Please change your account information via the store.", -1);
370        return -1;
371    }
372
373    /**
374     * Check the given password for the given entity (customer) against Magento
375     *
376     * The check is performed by comparing hashes
377     *
378     * @param   int    $entity the entity of the user
379     * @param   string $pass   the clear text password
380     */
381    protected function _checkUserPassword( $entity, $pass ) {
382        try {
383        // get a connection to the database
384            $dbh = new PDO( $this->db_dsn, $this->db_user, $this->db_passwd, array( PDO::ATTR_PERSISTENT => true ) );
385            $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
386
387        // query the password hash
388            $sql = "select a.entity_id entity, a.value hash from customer_entity_varchar a where a.attribute_id = 12 and a.entity_id = {$entity};";
389            $hash = "";
390            foreach( $dbh->query( $sql ) as $row ) {
391                if ( $entity === $row['entity'] ) {
392                    $hash = $row['hash'];
393                }
394            }
395            $dbh = null;
396        // compare in the same way as Magento
397            return $this->validateHash( $pass, $hash );
398        } catch (PDOException $e) {
399            if ( $this->getConf( 'debugDatabase' ) == 1 ) {
400                msg( $e->getMessage(), -1);
401            }
402        }
403        return false;
404    }
405
406    /**
407     * Find administrator entity from the administrator data
408     *
409     * The given user is matched against the "user" found in the Magento database
410     * All spaces in names are converted to the seperator character
411     *
412     * @param   string $user the user name
413     * @return  int          the entity found in the list of administrators or -1
414     */
415    protected function _findAdmin( $user ) {
416    // load the administrator data if not already
417        if( $this->admins === null ) $this->_loadAdminData();
418    // find the given administrator in the admin data
419        foreach( $this->admins as $entry ) {
420            $user_id = $entry['user'];
421            $user_id = cleanID( $user_id );
422            if ( strnatcasecmp ( $user , $user_id ) === 0 ) return $entry['entity'];
423        }
424        return -1;
425    }
426
427    /**
428     * Check the given password for the given entity (administrator) against Magento
429     *
430     * The check is performed by comparing hashes
431     *
432     * @param   int    $entity the entity of the user
433     * @param   string $pass   the clear text password
434     */
435    protected function _checkAdminPassword( $entity, $pass ) {
436        try {
437        // get a connection to the database
438            $dbh = new PDO( $this->db_dsn, $this->db_user, $this->db_passwd, array( PDO::ATTR_PERSISTENT => true ) );
439            $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
440
441        // query the password hash
442            $sql = "select user_id entity,password hash from admin_user where user_id = {$entity};";
443            $hash = "";
444            foreach( $dbh->query( $sql ) as $row ) {
445                if ( $entity === $row['entity'] ) {
446                    $hash = $row['hash'];
447                }
448            }
449            $dbh = null;
450        // compare in the same way as Magento
451            return $this->validateHash( $pass, $hash );
452        } catch (PDOException $e) {
453            if ( $this->getConf( 'debugDatabase' ) == 1 ) {
454                msg( $e->getMessage(), -1);
455            }
456        }
457        return false;
458    }
459
460// TAKE FROM MAGENTO SOURCE CODE 1.8.1
461    /**
462     * Hash a string
463     *
464     * @param string $data
465     * @return string
466     */
467    protected function hash($data)
468    {
469        return md5($data);
470    }
471
472    /**
473     * Validate hash against hashing method (with or without salt)
474     *
475     * @param string $password
476     * @param string $hash
477     * @return bool
478     */
479    protected function validateHash($password, $hash)
480    {
481        $hashArr = explode(':', $hash);
482        switch (count($hashArr)) {
483            case 1:
484                return $this->hash($password) === $hash;
485            case 2:
486                return $this->hash($hashArr[1] . $password) === $hashArr[0];
487        }
488        return false;
489    }
490// TAKE FROM MAGENTO SOURCE CODE 1.8.1
491
492    /**
493     * Load all user (customer) data from Magento, i.e. just the information require to identify the user
494     *
495     * @return bool
496     */
497    protected function _loadUserData() {
498        $this->users = array();
499        // query only if configured
500        if ( $this->getConf( 'includeCustomers' ) != 1 ) return true;
501
502        try {
503        // get a connection to the database
504            $dbh = new PDO( $this->db_dsn, $this->db_user, $this->db_passwd, array( PDO::ATTR_PERSISTENT => true ) );
505            $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
506
507        // query the user information
508            $sql = "select a.entity_id entity,a.value first,b.value last from customer_entity_varchar a,customer_entity_varchar b where a.entity_id = b.entity_id and a.attribute_id = 5 and b.attribute_id = 7;";
509            foreach( $dbh->query( $sql ) as $row ) {
510                $this->users[$row['entity']]['entity'] = $row['entity'];
511                $this->users[$row['entity']]['first']  = $row['first'];
512                $this->users[$row['entity']]['last']   = $row['last'];
513            }
514            $dbh = null;
515            return true;
516        } catch (PDOException $e) {
517            if ( $this->getConf( 'debugDatabase' ) == 1 ) {
518                msg( $e->getMessage(), -1);
519            }
520        }
521        return false;
522    }
523
524    /**
525     * Load the mail address of the given entity (customer) from Magento
526     *
527     * @return bool
528     */
529    protected function _loadMailAddress( $entity ) {
530        $this->users[$entity]['mail'] = "default";
531
532        try {
533        // get a connection to the database
534            $dbh = new PDO( $this->db_dsn, $this->db_user, $this->db_passwd, array( PDO::ATTR_PERSISTENT => true ) );
535            $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
536
537        // query the mail address
538            $sql = "select a.entity_id entity,a.email mail from customer_entity a where a.entity_id = {$entity};";
539            foreach( $dbh->query( $sql ) as $row ) {
540                if ( $entity === $row['entity'] ) {
541                    $this->users[$entity]['mail'] = $row['mail'];
542                }
543            }
544            $dbh = null;
545            return true;
546        } catch (PDOException $e) {
547            if ( $this->getConf( 'debugDatabase' ) == 1 ) {
548                msg( $e->getMessage(), -1);
549            }
550        }
551        return false;
552    }
553
554    /**
555     * Load the group of the given entity (customer) from Magento
556     *
557     * @return bool
558     */
559    protected function _loadUserGroups( $entity ) {
560        $this->users[$entity]['groups'] = array();
561        // query only if configured
562        if ( $this->getConf( 'includeGroups' ) != 1 ) return true;
563
564        try {
565        // get a connection to the database
566            $dbh = new PDO( $this->db_dsn, $this->db_user, $this->db_passwd, array( PDO::ATTR_PERSISTENT => true ) );
567            $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
568
569        // query the groups
570            $sql = "select a.entity_id entity,b.customer_group_code groups from customer_entity a, customer_group b where a.group_id = b.customer_group_id and a.entity_id = {$entity};";
571            foreach( $dbh->query( $sql ) as $row ) {
572                if ( $entity === $row['entity'] ) {
573                    $name = cleanID( $row['groups'] );
574                    $this->users[$entity]['groups'] = array( $name );
575                }
576            }
577            $dbh = null;
578            return true;
579        } catch (PDOException $e) {
580            if ( $this->getConf( 'debugDatabase' ) == 1 ) {
581                msg( $e->getMessage(), -1);
582            }
583        }
584        return false;
585    }
586
587    /**
588     * Load all administrator data from Magento, i.e. just the information require to identify the administrator
589     *
590     * @return bool
591     */
592    protected function _loadAdminData() {
593        $this->admins = array();
594        // query only if configured
595        if ( $this->getConf( 'includeAdmins' ) != 1 ) return true;
596
597        try {
598        // get a connection to the database
599            $dbh = new PDO( $this->db_dsn, $this->db_user, $this->db_passwd, array( PDO::ATTR_PERSISTENT => true ) );
600            $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
601
602        // query the administrator information
603            $sql = "select user_id entity,firstname first,lastname last, username user,email mail from admin_user where is_active = 1;";
604            foreach( $dbh->query( $sql ) as $row ) {
605                $this->admins[$row['entity']]['entity'] = $row['entity'];
606                $this->admins[$row['entity']]['user']   = $row['user'];
607                $this->admins[$row['entity']]['first']  = $row['first'];
608                $this->admins[$row['entity']]['last']   = $row['last'];
609                $this->admins[$row['entity']]['mail']   = $row['mail'];
610            }
611            $dbh = null;
612            return true;
613        } catch (PDOException $e) {
614            if ( $this->getConf( 'debugDatabase' ) == 1 ) {
615                msg( $e->getMessage(), -1);
616            }
617        }
618        return false;
619    }
620
621    /**
622     * Load the roles of the given entity (administrator) from Magento
623     *
624     * @return bool
625     */
626    protected function _loadAdminRoles( $entity ) {
627        $this->admins[$entity]['roles'] = array();
628        // query only if configured
629        if ( $this->getConf( 'includeRoles' ) != 1 ) return true;
630
631        if( $this->roles === null ) $this->_loadRoles();
632
633        $stack = array();
634        try {
635        // get a connection to the database
636            $dbh = new PDO( $this->db_dsn, $this->db_user, $this->db_passwd, array( PDO::ATTR_PERSISTENT => true ) );
637            $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
638
639        // find the top level role for the administrator
640            $sql = "select role_id id,role_type type,parent_id parent,tree_level level from admin_role where user_id = {$entity} order by tree_level desc,parent_id desc;";
641            foreach( $dbh->query( $sql ) as $row ) {
642                $xarray = array( $row['id'], $row['type'], $row['parent'], $row['level'] );
643                array_push( $stack, $xarray );
644            }
645        // and search for groups (roles of type G)
646            $item = array_shift( $stack );
647            while ( $item != null ) {
648                if ( $item[1] == "G" ) {
649                    $name = $this->roles[$item[0]]['name'];
650                    if ( $name != null ) {
651                        $name = cleanID( $name );
652                        array_push( $this->admins[$entity]['roles'] , $name );
653                    }
654                }
655
656                if ( $item[1] == "U" ) {
657            // query the parent role
658                    $sql = "select role_id id,role_type type,parent_id parent,tree_level level from admin_role where role_id = {$item[2]} order by tree_level desc,parent_id desc;";
659                    foreach( $dbh->query( $sql ) as $row ) {
660                        $xarray = array( $row['id'], $row['type'], $row['parent'], $row['level'] );
661                        array_push( $stack, $xarray );
662                    }
663                }
664
665                $item = array_shift( $stack );
666            }
667            $dbh = null;
668            return true;
669        } catch (PDOException $e) {
670            if ( $this->getConf( 'debugDatabase' ) == 1 ) {
671                msg( $e->getMessage(), -1);
672            }
673        }
674        return false;
675    }
676
677    /**
678     * Load all administrator roles from Magento
679     *
680     * @return bool
681     */
682    protected function _loadRoles() {
683        $this->roles = array();
684
685        try {
686        // get a connection to the database
687            $dbh = new PDO( $this->db_dsn, $this->db_user, $this->db_passwd, array( PDO::ATTR_PERSISTENT => true ) );
688            $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
689
690        // query the administrator roles
691            $sql = "select role_id id,role_name name from admin_role where role_type = 'G' order by role_id;";
692            foreach( $dbh->query( $sql ) as $row ) {
693                $this->roles[$row['id']]['id']   = $row['id'];
694                $this->roles[$row['id']]['name'] = $row['name'];
695            }
696            $dbh = null;
697            return true;
698        } catch (PDOException $e) {
699            if ( $this->getConf( 'debugDatabase' ) == 1 ) {
700                msg( $e->getMessage(), -1);
701            }
702        }
703        return false;
704    }
705}
706
707// vim:ts=4:sw=4:et:
708