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
17 if(!defined('DOKU_INC')) die();
18 
19 class 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