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