|
eZ Publish
[trunk]
|
00001 <?php 00002 /** 00003 * File containing the eZUser class. 00004 * 00005 * @copyright Copyright (C) 1999-2012 eZ Systems AS. All rights reserved. 00006 * @license http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2 00007 * @version //autogentag// 00008 * @package kernel 00009 */ 00010 00011 /*! 00012 \class eZUser ezuser.php 00013 \brief eZUser handles eZ Publish user accounts 00014 \ingroup eZDatatype 00015 00016 */ 00017 00018 class eZUser extends eZPersistentObject 00019 { 00020 /// MD5 of password 00021 const PASSWORD_HASH_MD5_PASSWORD = 1; 00022 /// MD5 of user and password 00023 const PASSWORD_HASH_MD5_USER = 2; 00024 /// MD5 of site, user and password 00025 const PASSWORD_HASH_MD5_SITE = 3; 00026 /// Legacy support for mysql hashed passwords 00027 const PASSWORD_HASH_MYSQL = 4; 00028 /// Passwords in plaintext, should not be used for real sites 00029 const PASSWORD_HASH_PLAINTEXT = 5; 00030 // Crypted passwords 00031 const PASSWORD_HASH_CRYPT = 6; 00032 00033 /// Authenticate by matching the login field 00034 const AUTHENTICATE_LOGIN = 1; 00035 /// Authenticate by matching the email field 00036 const AUTHENTICATE_EMAIL = 2; 00037 00038 const AUTHENTICATE_ALL = 3; //EZ_USER_AUTHENTICATE_LOGIN | EZ_USER_AUTHENTICATE_EMAIL; 00039 00040 /** 00041 * Flag used to prevent session regeneration 00042 * 00043 * @var int 00044 * @see eZUser::setCurrentlyLoggedInUser() 00045 */ 00046 const NO_SESSION_REGENERATE = 1; 00047 00048 protected static $anonymousId = null; 00049 00050 function eZUser( $row = array() ) 00051 { 00052 $this->eZPersistentObject( $row ); 00053 $this->OriginalPassword = false; 00054 $this->OriginalPasswordConfirm = false; 00055 } 00056 00057 static function definition() 00058 { 00059 static $definition = array( 'fields' => array( 'contentobject_id' => array( 'name' => 'ContentObjectID', 00060 'datatype' => 'integer', 00061 'default' => 0, 00062 'required' => true, 00063 'foreign_class' => 'eZContentObject', 00064 'foreign_attribute' => 'id', 00065 'multiplicity' => '0..1' ), 00066 'login' => array( 'name' => 'Login', 00067 'datatype' => 'string', 00068 'default' => '', 00069 'required' => true ), 00070 'email' => array( 'name' => 'Email', 00071 'datatype' => 'string', 00072 'default' => '', 00073 'required' => true ), 00074 'password_hash' => array( 'name' => 'PasswordHash', 00075 'datatype' => 'string', 00076 'default' => '', 00077 'required' => true ), 00078 'password_hash_type' => array( 'name' => 'PasswordHashType', 00079 'datatype' => 'integer', 00080 'default' => 1, 00081 'required' => true ) ), 00082 'keys' => array( 'contentobject_id' ), 00083 'sort' => array( 'contentobject_id' => 'asc' ), 00084 'function_attributes' => array( 'contentobject' => 'contentObject', 00085 'account_key' => 'accountKey', 00086 'groups' => 'groups', 00087 'has_stored_login' => 'hasStoredLogin', 00088 'original_password' => 'originalPassword', 00089 'original_password_confirm' => 'originalPasswordConfirm', 00090 'roles' => 'roles', 00091 'role_id_list' => 'roleIDList', 00092 'limited_assignment_value_list' => 'limitValueList', 00093 'is_logged_in' => 'isLoggedIn', 00094 'is_enabled' => 'isEnabled', 00095 'is_locked' => 'isLocked', 00096 'last_visit' => 'lastVisit', 00097 'login_count' => 'loginCount', 00098 'has_manage_locations' => 'hasManageLocations' ), 00099 'relations' => array( 'contentobject_id' => array( 'class' => 'ezcontentobject', 00100 'field' => 'id' ) ), 00101 'class_name' => 'eZUser', 00102 'name' => 'ezuser' ); 00103 return $definition; 00104 } 00105 00106 /*! 00107 \return a textual identifier for the hash type $id 00108 */ 00109 static function passwordHashTypeName( $id ) 00110 { 00111 switch ( $id ) 00112 { 00113 case self::PASSWORD_HASH_MD5_PASSWORD: 00114 { 00115 return 'md5_password'; 00116 } break; 00117 case self::PASSWORD_HASH_MD5_USER: 00118 { 00119 return 'md5_user'; 00120 } break; 00121 case self::PASSWORD_HASH_MD5_SITE: 00122 { 00123 return 'md5_site'; 00124 } break; 00125 case self::PASSWORD_HASH_MYSQL: 00126 { 00127 return 'mysql'; 00128 } break; 00129 case self::PASSWORD_HASH_PLAINTEXT: 00130 { 00131 return 'plaintext'; 00132 } break; 00133 case self::PASSWORD_HASH_CRYPT: 00134 { 00135 return 'crypt'; 00136 } break; 00137 } 00138 } 00139 00140 /*! 00141 \return the hash type for the textual identifier $identifier 00142 */ 00143 static function passwordHashTypeID( $identifier ) 00144 { 00145 switch ( $identifier ) 00146 { 00147 case 'md5_password': 00148 { 00149 return self::PASSWORD_HASH_MD5_PASSWORD; 00150 } break; 00151 default: 00152 case 'md5_user': 00153 { 00154 return self::PASSWORD_HASH_MD5_USER; 00155 } break; 00156 case 'md5_site': 00157 { 00158 return self::PASSWORD_HASH_MD5_SITE; 00159 } break; 00160 case 'mysql': 00161 { 00162 return self::PASSWORD_HASH_MYSQL; 00163 } break; 00164 case 'plaintext': 00165 { 00166 return self::PASSWORD_HASH_PLAINTEXT; 00167 } break; 00168 case 'crypt': 00169 { 00170 return self::PASSWORD_HASH_CRYPT; 00171 } break; 00172 } 00173 } 00174 00175 /*! 00176 Check if current user has "content/manage_locations" access 00177 */ 00178 function hasManageLocations() 00179 { 00180 $accessResult = $this->hasAccessTo( 'content', 'manage_locations' ); 00181 if ( $accessResult['accessWord'] != 'no' ) 00182 { 00183 return true; 00184 } 00185 00186 return false; 00187 } 00188 00189 static function create( $contentObjectID ) 00190 { 00191 $row = array( 00192 'contentobject_id' => $contentObjectID, 00193 'login' => null, 00194 'email' => null, 00195 'password_hash' => null, 00196 'password_hash_type' => null 00197 ); 00198 return new eZUser( $row ); 00199 } 00200 00201 function store( $fieldFilters = null ) 00202 { 00203 $this->Email = trim( $this->Email ); 00204 $userID = $this->attribute( 'contentobject_id' ); 00205 // Clear memory cache 00206 unset( $GLOBALS['eZUserObject_' . $userID] ); 00207 $GLOBALS['eZUserObject_' . $userID] = $this; 00208 self::purgeUserCacheByUserId( $userID ); 00209 eZPersistentObject::store( $fieldFilters ); 00210 } 00211 00212 function originalPassword() 00213 { 00214 return $this->OriginalPassword; 00215 } 00216 00217 function setOriginalPassword( $password ) 00218 { 00219 $this->OriginalPassword = $password; 00220 } 00221 00222 function originalPasswordConfirm() 00223 { 00224 return $this->OriginalPasswordConfirm; 00225 } 00226 00227 function setOriginalPasswordConfirm( $password ) 00228 { 00229 $this->OriginalPasswordConfirm = $password; 00230 } 00231 00232 function hasStoredLogin() 00233 { 00234 $db = eZDB::instance(); 00235 $contentObjectID = $this->attribute( 'contentobject_id' ); 00236 $sql = "SELECT * FROM ezuser WHERE contentobject_id='$contentObjectID' AND LENGTH( login ) > 0"; 00237 $rows = $db->arrayQuery( $sql ); 00238 return !empty( $rows ); 00239 } 00240 00241 /*! 00242 Fills in the \a $id, \a $login, \a $email and \a $password for the user 00243 and creates the proper password hash. 00244 */ 00245 function setInformation( $id, $login, $email, $password, $passwordConfirm = false ) 00246 { 00247 $this->setAttribute( "contentobject_id", $id ); 00248 $this->setAttribute( "email", $email ); 00249 $this->setAttribute( "login", $login ); 00250 if ( eZUser::validatePassword( $password ) and 00251 $password == $passwordConfirm ) // Cannot change login or password_hash without login and password 00252 { 00253 $this->setAttribute( "password_hash", eZUser::createHash( $login, $password, eZUser::site(), 00254 eZUser::hashType() ) ); 00255 $this->setAttribute( "password_hash_type", eZUser::hashType() ); 00256 } 00257 else 00258 { 00259 $this->setOriginalPassword( $password ); 00260 $this->setOriginalPasswordConfirm( $passwordConfirm ); 00261 } 00262 } 00263 00264 static function fetch( $id, $asObject = true ) 00265 { 00266 if ( !$id ) 00267 return null; 00268 return eZPersistentObject::fetchObject( eZUser::definition(), 00269 null, 00270 array( 'contentobject_id' => $id ), 00271 $asObject ); 00272 } 00273 00274 static function fetchByName( $login, $asObject = true ) 00275 { 00276 return eZPersistentObject::fetchObject( eZUser::definition(), 00277 null, 00278 array( 'LOWER( login )' => strtolower( $login ) ), 00279 $asObject ); 00280 } 00281 00282 static function fetchByEmail( $email, $asObject = true ) 00283 { 00284 return eZPersistentObject::fetchObject( eZUser::definition(), 00285 null, 00286 array( 'LOWER( email )' => strtolower( $email ) ), 00287 $asObject ); 00288 } 00289 00290 /** 00291 * Return an array of unactivated eZUser object 00292 * 00293 * @param array|false|null An associative array of sorting conditions, 00294 * if set to false ignores settings in $def, if set to null uses 00295 * settingss in $def. 00296 * @param int $limit 00297 * @param int $offset 00298 * @return array( eZUser ) 00299 */ 00300 static public function fetchUnactivated( $sort = false, $limit = 10, $offset = 0 ) 00301 { 00302 $accountDef = eZUserAccountKey::definition(); 00303 $settingsDef = eZUserSetting::definition(); 00304 00305 return eZPersistentObject::fetchObjectList( 00306 eZUser::definition(), null, null, $sort, 00307 array( 00308 'limit' => $limit, 00309 'offset' => $offset 00310 ), 00311 true, false, null, 00312 array( $accountDef['name'], $settingsDef['name'] ), 00313 " WHERE contentobject_id = {$accountDef['name']}.user_id" 00314 . " AND {$settingsDef['name']}.user_id = contentobject_id" 00315 . " AND is_enabled = 0" 00316 ); 00317 } 00318 00319 /*! 00320 \static 00321 \return a list of the logged in users. 00322 \param $asObject If false it will return a list with only the names of the users as elements and user ID as key, 00323 otherwise each entry is a eZUser object. 00324 \sa fetchLoggedInCount 00325 */ 00326 static function fetchLoggedInList( $asObject = false, $offset = false, $limit = false, $sortBy = false ) 00327 { 00328 $db = eZDB::instance(); 00329 $time = time() - eZINI::instance()->variable( 'Session', 'ActivityTimeout' ); 00330 00331 $parameters = array(); 00332 if ( $offset ) 00333 $parameters['offset'] =(int) $offset; 00334 if ( $limit ) 00335 $parameters['limit'] =(int) $limit; 00336 $sortText = ''; 00337 if ( $asObject ) 00338 { 00339 $selectArray = array( "distinct ezuser.*" ); 00340 } 00341 else 00342 { 00343 $selectArray = array( "ezuser.contentobject_id as user_id", "ezcontentobject.name" ); 00344 } 00345 if ( $sortBy !== false ) 00346 { 00347 $sortElements = array(); 00348 if ( !is_array( $sortBy ) ) 00349 { 00350 $sortBy = array( array( $sortBy, true ) ); 00351 } 00352 else if ( !is_array( $sortBy[0] ) ) 00353 $sortBy = array( $sortBy ); 00354 $sortColumns = array(); 00355 foreach ( $sortBy as $sortElements ) 00356 { 00357 $sortColumn = $sortElements[0]; 00358 $sortOrder = $sortElements[1]; 00359 $orderText = $sortOrder ? 'asc' : 'desc'; 00360 switch ( $sortColumn ) 00361 { 00362 case 'user_id': 00363 { 00364 $sortColumn = "ezuser.contentobject_id $orderText"; 00365 } break; 00366 00367 case 'login': 00368 { 00369 $sortColumn = "ezuser.login $orderText"; 00370 } break; 00371 00372 case 'activity': 00373 { 00374 $selectArray[] = "ezuservisit.current_visit_timestamp AS activity"; 00375 $sortColumn = "activity $orderText"; 00376 } break; 00377 00378 case 'email': 00379 { 00380 $sortColumn = "ezuser.email $orderText"; 00381 } break; 00382 00383 default: 00384 { 00385 eZDebug::writeError( "Unkown sort column '$sortColumn'", __METHOD__ ); 00386 $sortColumn = false; 00387 } break; 00388 } 00389 if ( $sortColumn ) 00390 $sortColumns[] = $sortColumn; 00391 } 00392 if ( !empty( $sortColumns ) ) 00393 $sortText = "ORDER BY " . implode( ', ', $sortColumns ); 00394 } 00395 if ( $asObject ) 00396 { 00397 $selectText = implode( ', ', $selectArray ); 00398 $sql = "SELECT $selectText 00399 FROM ezuservisit, ezuser 00400 WHERE ezuservisit.user_id != '" . self::anonymousId() . "' AND 00401 ezuservisit.current_visit_timestamp > '$time' AND 00402 ezuser.contentobject_id = ezuservisit.user_id 00403 $sortText"; 00404 $rows = $db->arrayQuery( $sql, $parameters ); 00405 $list = array(); 00406 foreach ( $rows as $row ) 00407 { 00408 $list[] = new eZUser( $row ); 00409 } 00410 } 00411 else 00412 { 00413 $selectText = implode( ', ', $selectArray ); 00414 $sql = "SELECT $selectText 00415 FROM ezuservisit, ezuser, ezcontentobject 00416 WHERE ezuservisit.user_id != '" . self::anonymousId() . "' AND 00417 ezuservisit.current_visit_timestamp > '$time' AND 00418 ezuser.contentobject_id = ezuservisit.user_id AND 00419 ezcontentobject.id = ezuser.contentobject_id 00420 $sortText"; 00421 $rows = $db->arrayQuery( $sql, $parameters ); 00422 $list = array(); 00423 foreach ( $rows as $row ) 00424 { 00425 $list[$row['user_id']] = $row['name']; 00426 } 00427 } 00428 return $list; 00429 } 00430 00431 /*! 00432 \return the number of logged in users in the system. 00433 \note The count will be cached for the current page if caching is allowed. 00434 \sa fetchAnonymousCount 00435 */ 00436 static function fetchLoggedInCount() 00437 { 00438 if ( isset( $GLOBALS['eZSiteBasics']['no-cache-adviced'] ) and 00439 !$GLOBALS['eZSiteBasics']['no-cache-adviced'] and 00440 isset( $GLOBALS['eZUserLoggedInCount'] ) ) 00441 return $GLOBALS['eZUserLoggedInCount']; 00442 $db = eZDB::instance(); 00443 $time = time() - eZINI::instance()->variable( 'Session', 'ActivityTimeout' ); 00444 00445 $sql = "SELECT count( DISTINCT user_id ) as count 00446 FROM ezuservisit 00447 WHERE user_id != '" . self::anonymousId() . "' AND 00448 user_id > 0 AND 00449 current_visit_timestamp > '$time'"; 00450 $rows = $db->arrayQuery( $sql ); 00451 $count = isset( $rows[0] ) ? $rows[0]['count'] : 0; 00452 $GLOBALS['eZUserLoggedInCount'] = $count; 00453 return $count; 00454 } 00455 00456 /** 00457 * Return the number of anonymous users in the system. 00458 * 00459 * @deprecated As of 4.4 since default session handler does not support this. 00460 * @return int 00461 */ 00462 static function fetchAnonymousCount() 00463 { 00464 if ( isset( $GLOBALS['eZSiteBasics']['no-cache-adviced'] ) and 00465 !$GLOBALS['eZSiteBasics']['no-cache-adviced'] and 00466 isset( $GLOBALS['eZUserAnonymousCount'] ) ) 00467 return $GLOBALS['eZUserAnonymousCount']; 00468 $db = eZDB::instance(); 00469 $time = time(); 00470 $ini = eZINI::instance(); 00471 $activityTimeout = $ini->variable( 'Session', 'ActivityTimeout' ); 00472 $sessionTimeout = $ini->variable( 'Session', 'SessionTimeout' ); 00473 $time = $time + $sessionTimeout - $activityTimeout; 00474 00475 $sql = "SELECT count( session_key ) as count 00476 FROM ezsession 00477 WHERE user_id = '" . self::anonymousId() . "' AND 00478 expiration_time > '$time'"; 00479 $rows = $db->arrayQuery( $sql ); 00480 $count = isset( $rows[0] ) ? $rows[0]['count'] : 0; 00481 $GLOBALS['eZUserAnonymousCount'] = $count; 00482 return $count; 00483 } 00484 00485 /*! 00486 \static 00487 \return true if the user with ID $userID is currently logged into the system. 00488 \note The information will be cached for the current page if caching is allowed. 00489 \sa fetchLoggedInList 00490 */ 00491 static function isUserLoggedIn( $userID ) 00492 { 00493 $userID = (int)$userID; 00494 if ( isset( $GLOBALS['eZSiteBasics']['no-cache-adviced'] ) and 00495 !$GLOBALS['eZSiteBasics']['no-cache-adviced'] and 00496 isset( $GLOBALS['eZUserLoggedInMap'][$userID] ) ) 00497 return $GLOBALS['eZUserLoggedInMap'][$userID]; 00498 $db = eZDB::instance(); 00499 $time = time() - eZINI::instance()->variable( 'Session', 'ActivityTimeout' ); 00500 00501 $sql = "SELECT DISTINCT user_id 00502 FROM ezuservisit 00503 WHERE user_id = '" . $userID . "' AND 00504 current_visit_timestamp > '$time'"; 00505 $rows = $db->arrayQuery( $sql, array( 'limit' => 2 ) ); 00506 $isLoggedIn = isset( $rows[0] ); 00507 $GLOBALS['eZUserLoggedInMap'][$userID] = $isLoggedIn; 00508 return $isLoggedIn; 00509 } 00510 00511 /*! 00512 \static 00513 Removes any cached session information, this is: 00514 - logged in user count 00515 - anonymous user count 00516 - logged in user map 00517 */ 00518 static function clearSessionCache() 00519 { 00520 unset( $GLOBALS['eZUserLoggedInCount'] ); 00521 unset( $GLOBALS['eZUserAnonymousCount'] ); 00522 unset( $GLOBALS['eZUserLoggedInMap'] ); 00523 } 00524 00525 /** 00526 * Remove session data for user \a $userID. 00527 * @todo should use eZSession api (needs to be created) so 00528 * callbacks (like preference / basket..) is cleared as well. 00529 * 00530 * @params int $userID 00531 */ 00532 static function removeSessionData( $userID ) 00533 { 00534 eZUser::clearSessionCache(); 00535 eZSession::getHandlerInstance()->deleteByUserIDs( array( $userID ) ); 00536 } 00537 00538 /*! 00539 Removes the user from the ezuser table. 00540 \note Will also remove any notifications and session related to the user. 00541 */ 00542 static function removeUser( $userID ) 00543 { 00544 $user = eZUser::fetch( $userID ); 00545 if ( !$user ) 00546 { 00547 eZDebug::writeError( "unable to find user with ID $userID", __METHOD__ ); 00548 return false; 00549 } 00550 00551 eZUser::removeSessionData( $userID ); 00552 00553 eZSubtreeNotificationRule::removeByUserID( $userID ); 00554 eZCollaborationNotificationRule::removeByUserID( $userID ); 00555 eZUserSetting::removeByUserID( $userID ); 00556 eZUserAccountKey::removeByUserID( $userID ); 00557 eZForgotPassword::removeByUserID( $userID ); 00558 eZWishList::removeByUserID( $userID ); 00559 00560 // only remove general digest setting if there are no other users with the same e-mail 00561 $email = $user->attribute( 'email' ); 00562 $usersWithEmailCount = eZPersistentObject::count( eZUser::definition(), array( 'email' => $email ) ); 00563 if ( $usersWithEmailCount == 1 ) 00564 { 00565 eZGeneralDigestUserSettings::removeByAddress( $email ); 00566 } 00567 00568 eZPersistentObject::removeObject( eZUser::definition(), 00569 array( 'contentobject_id' => $userID ) ); 00570 return true; 00571 } 00572 00573 /*! 00574 \return a list of valid and enabled users, the data returned is an array 00575 with ezcontentobject database data. 00576 */ 00577 static function fetchContentList() 00578 { 00579 $contentObjectStatus = eZContentObject::STATUS_PUBLISHED; 00580 $query = "SELECT ezcontentobject.* 00581 FROM ezuser, ezcontentobject, ezuser_setting 00582 WHERE ezcontentobject.status = '$contentObjectStatus' AND 00583 ezuser_setting.is_enabled = 1 AND 00584 ezcontentobject.id = ezuser.contentobject_id AND 00585 ezuser_setting.user_id = ezuser.contentobject_id"; 00586 $db = eZDB::instance(); 00587 $rows = $db->arrayQuery( $query ); 00588 return $rows; 00589 } 00590 00591 /*! 00592 \static 00593 \return the default hash type which is specified in UserSettings/HashType in site.ini 00594 */ 00595 static function hashType() 00596 { 00597 $ini = eZINI::instance(); 00598 $type = strtolower( $ini->variable( 'UserSettings', 'HashType' ) ); 00599 if ( $type == 'md5_site' ) 00600 return self::PASSWORD_HASH_MD5_SITE; 00601 else if ( $type == 'md5_user' ) 00602 return self::PASSWORD_HASH_MD5_USER; 00603 else if ( $type == 'plaintext' ) 00604 return self::PASSWORD_HASH_PLAINTEXT; 00605 else if ( $type == 'crypt' ) 00606 return self::PASSWORD_HASH_CRYPT; 00607 else 00608 return self::PASSWORD_HASH_MD5_PASSWORD; 00609 } 00610 00611 /*! 00612 \static 00613 \return the site name used in password hashing. 00614 */ 00615 static function site() 00616 { 00617 $ini = eZINI::instance(); 00618 return $ini->variable( 'UserSettings', 'SiteName' ); 00619 } 00620 00621 /*! 00622 Fetches a builtin user and returns it, this helps avoid special cases where 00623 user is not logged in. 00624 */ 00625 static function fetchBuiltin( $id ) 00626 { 00627 if ( !in_array( $id, $GLOBALS['eZUserBuiltins'] ) ) 00628 $id = self::anonymousId(); 00629 if ( empty( $GLOBALS["eZUserBuilitinInstance-$id"] ) ) 00630 { 00631 $GLOBALS["eZUserBuilitinInstance-$id"] = eZUser::fetch( self::anonymousId() ); 00632 } 00633 return $GLOBALS["eZUserBuilitinInstance-$id"]; 00634 } 00635 00636 /*! 00637 \return the user id. 00638 */ 00639 function id() 00640 { 00641 return $this->ContentObjectID; 00642 } 00643 00644 /*! 00645 \return a bitfield which decides the authenticate methods. 00646 */ 00647 static function authenticationMatch() 00648 { 00649 $ini = eZINI::instance(); 00650 $matchArray = $ini->variableArray( 'UserSettings', 'AuthenticateMatch' ); 00651 $match = 0; 00652 foreach ( $matchArray as $matchItem ) 00653 { 00654 switch ( $matchItem ) 00655 { 00656 case "login": 00657 { 00658 $match = ( $match | self::AUTHENTICATE_LOGIN ); 00659 } break; 00660 case "email": 00661 { 00662 $match = ( $match | self::AUTHENTICATE_EMAIL ); 00663 } break; 00664 } 00665 } 00666 return $match; 00667 } 00668 00669 /*! 00670 \return \c true if there can only be one instance of an email address on the site. 00671 */ 00672 static function requireUniqueEmail() 00673 { 00674 $ini = eZINI::instance(); 00675 return $ini->variable( 'UserSettings', 'RequireUniqueEmail' ) == 'true'; 00676 } 00677 00678 /** 00679 * Logs in the user if applied username and password is valid. 00680 * 00681 * @param string $login 00682 * @param string $password 00683 * @param bool $authenticationMatch 00684 * @return mixed eZUser on success, bool false on failure 00685 */ 00686 public static function loginUser( $login, $password, $authenticationMatch = false ) 00687 { 00688 $user = self::_loginUser( $login, $password, $authenticationMatch ); 00689 00690 if ( is_object( $user ) ) 00691 { 00692 self::loginSucceeded( $user ); 00693 return $user; 00694 } 00695 else 00696 { 00697 self::loginFailed( $user, $login ); 00698 return false; 00699 } 00700 } 00701 00702 /** 00703 * Does some house keeping work once a log in has succeeded. 00704 * 00705 * @param eZUser $user 00706 */ 00707 protected static function loginSucceeded( $user ) 00708 { 00709 $userID = $user->attribute( 'contentobject_id' ); 00710 00711 // if audit is enabled logins should be logged 00712 eZAudit::writeAudit( 'user-login', array( 'User id' => $userID, 'User login' => $user->attribute( 'login' ) ) ); 00713 00714 eZUser::updateLastVisit( $userID, true ); 00715 eZUser::setCurrentlyLoggedInUser( $user, $userID ); 00716 00717 // Reset number of failed login attempts 00718 eZUser::setFailedLoginAttempts( $userID, 0 ); 00719 } 00720 00721 /** 00722 * Does some house keeping work when a log in has failed. 00723 * 00724 * @param mixed $userID 00725 * @param string $login 00726 */ 00727 protected static function loginFailed( $userID = false, $login ) 00728 { 00729 $loginEscaped = eZDB::instance()->escapeString( $login ); 00730 00731 // Failed login attempts should be logged 00732 eZAudit::writeAudit( 'user-failed-login', array( 'User login' => $loginEscaped, 00733 'Comment' => 'Failed login attempt: eZUser::loginUser()' ) ); 00734 00735 // Increase number of failed login attempts. 00736 if ( $userID ) 00737 eZUser::setFailedLoginAttempts( $userID ); 00738 } 00739 00740 /** 00741 * Logs in an user if applied login and password is valid. 00742 * 00743 * This method does not do any house keeping work anymore (writing audits, etc). 00744 * When you call this method make sure to call loginSucceeded() or loginFailed() 00745 * depending on the success of the login. 00746 * 00747 * @param string $login 00748 * @param string $password 00749 * @param bool $authenticationMatch 00750 * @return mixed eZUser object on log in success, int userID if the username 00751 * exists but log in failed, or false if the username doesn't exists. 00752 */ 00753 protected static function _loginUser( $login, $password, $authenticationMatch = false ) 00754 { 00755 $http = eZHTTPTool::instance(); 00756 $db = eZDB::instance(); 00757 00758 if ( $authenticationMatch === false ) 00759 $authenticationMatch = eZUser::authenticationMatch(); 00760 00761 $loginEscaped = $db->escapeString( $login ); 00762 $passwordEscaped = $db->escapeString( $password ); 00763 00764 $loginArray = array(); 00765 if ( $authenticationMatch & self::AUTHENTICATE_LOGIN ) 00766 $loginArray[] = "login='$loginEscaped'"; 00767 if ( $authenticationMatch & self::AUTHENTICATE_EMAIL ) 00768 { 00769 if ( eZMail::validate( $login ) ) 00770 { 00771 $loginArray[] = "email='$loginEscaped'"; 00772 } 00773 } 00774 if ( empty( $loginArray ) ) 00775 $loginArray[] = "login='$loginEscaped'"; 00776 $loginText = implode( ' OR ', $loginArray ); 00777 00778 $contentObjectStatus = eZContentObject::STATUS_PUBLISHED; 00779 00780 $ini = eZINI::instance(); 00781 $databaseName = $db->databaseName(); 00782 // if mysql 00783 if ( $databaseName === 'mysql' ) 00784 { 00785 $query = "SELECT contentobject_id, password_hash, password_hash_type, email, login 00786 FROM ezuser, ezcontentobject 00787 WHERE ( $loginText ) AND 00788 ezcontentobject.status='$contentObjectStatus' AND 00789 ezcontentobject.id=contentobject_id AND 00790 ( ( password_hash_type!=4 ) OR 00791 ( password_hash_type=4 AND 00792 ( $loginText ) AND 00793 password_hash=PASSWORD('$passwordEscaped') ) )"; 00794 } 00795 else 00796 { 00797 $query = "SELECT contentobject_id, password_hash, 00798 password_hash_type, email, login 00799 FROM ezuser, ezcontentobject 00800 WHERE ( $loginText ) 00801 AND ezcontentobject.status='$contentObjectStatus' 00802 AND ezcontentobject.id=contentobject_id"; 00803 } 00804 00805 $users = $db->arrayQuery( $query ); 00806 $exists = false; 00807 if ( $users !== false && isset( $users[0] ) ) 00808 { 00809 $ini = eZINI::instance(); 00810 foreach ( $users as $userRow ) 00811 { 00812 $userID = $userRow['contentobject_id']; 00813 $hashType = $userRow['password_hash_type']; 00814 $hash = $userRow['password_hash']; 00815 $exists = eZUser::authenticateHash( $userRow['login'], $password, eZUser::site(), 00816 $hashType, 00817 $hash ); 00818 00819 // If hash type is MySql 00820 if ( $hashType == self::PASSWORD_HASH_MYSQL and $databaseName === 'mysql' ) 00821 { 00822 $queryMysqlUser = "SELECT contentobject_id, password_hash, password_hash_type, email, login 00823 FROM ezuser, ezcontentobject 00824 WHERE ezcontentobject.status='$contentObjectStatus' AND 00825 password_hash_type=4 AND ( $loginText ) AND password_hash=PASSWORD('$passwordEscaped') "; 00826 $mysqlUsers = $db->arrayQuery( $queryMysqlUser ); 00827 if ( isset( $mysqlUsers[0] ) ) 00828 $exists = true; 00829 00830 } 00831 00832 eZDebugSetting::writeDebug( 'kernel-user', eZUser::createHash( $userRow['login'], $password, eZUser::site(), 00833 $hashType, $hash ), "check hash" ); 00834 eZDebugSetting::writeDebug( 'kernel-user', $hash, "stored hash" ); 00835 // If current user has been disabled after a few failed login attempts. 00836 $canLogin = eZUser::isEnabledAfterFailedLogin( $userID ); 00837 00838 if ( $exists ) 00839 { 00840 // We should store userID for warning message. 00841 $GLOBALS['eZFailedLoginAttemptUserID'] = $userID; 00842 00843 $userSetting = eZUserSetting::fetch( $userID ); 00844 $isEnabled = $userSetting->attribute( "is_enabled" ); 00845 if ( $hashType != eZUser::hashType() and 00846 strtolower( $ini->variable( 'UserSettings', 'UpdateHash' ) ) == 'true' ) 00847 { 00848 $hashType = eZUser::hashType(); 00849 $hash = eZUser::createHash( $userRow['login'], $password, eZUser::site(), 00850 $hashType ); 00851 $db->query( "UPDATE ezuser SET password_hash='$hash', password_hash_type='$hashType' WHERE contentobject_id='$userID'" ); 00852 } 00853 break; 00854 } 00855 } 00856 } 00857 00858 if ( $exists and $isEnabled and $canLogin ) 00859 { 00860 return new eZUser( $userRow ); 00861 } 00862 else 00863 { 00864 return isset( $userID ) ? $userID : false; 00865 } 00866 } 00867 00868 /*! 00869 \static 00870 Checks if IP address of current user is in \a $ipList. 00871 */ 00872 static function isUserIPInList( $ipList ) 00873 { 00874 $ipAddress = eZSys::clientIP(); 00875 if ( $ipAddress ) 00876 { 00877 $result = false; 00878 foreach( $ipList as $itemToMatch ) 00879 { 00880 if ( preg_match("/^(([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+))(\/([0-9]+)$|$)/", $itemToMatch, $matches ) ) 00881 { 00882 if ( $matches[6] ) 00883 { 00884 if ( eZDebug::isIPInNet( $ipAddress, $matches[1], $matches[7] ) ) 00885 { 00886 $result = true; 00887 break; 00888 } 00889 } 00890 else 00891 { 00892 if ( $matches[1] == $ipAddress ) 00893 { 00894 $result = true; 00895 break; 00896 } 00897 } 00898 } 00899 } 00900 } 00901 else 00902 { 00903 $result = ( 00904 in_array( 'commandline', $ipList ) && 00905 ( php_sapi_name() == 'cli' ) 00906 ); 00907 } 00908 return $result; 00909 } 00910 00911 /*! 00912 \static 00913 Returns true if current user is trusted user. 00914 */ 00915 static function isTrusted() 00916 { 00917 $ini = eZINI::instance(); 00918 00919 // Check if current user is trusted user. 00920 $trustedIPs = $ini->hasVariable( 'UserSettings', 'TrustedIPList' ) ? $ini->variable( 'UserSettings', 'TrustedIPList' ) : array(); 00921 00922 // Check if IP address of current user is in $trustedIPs array. 00923 $trustedUser = eZUser::isUserIPInList( $trustedIPs ); 00924 if ( $trustedUser ) 00925 return true; 00926 00927 return false; 00928 } 00929 00930 /*! 00931 \static 00932 Returns max number of failed login attempts. 00933 */ 00934 static function maxNumberOfFailedLogin() 00935 { 00936 $ini = eZINI::instance(); 00937 00938 $maxNumberOfFailedLogin = $ini->hasVariable( 'UserSettings', 'MaxNumberOfFailedLogin' ) ? $ini->variable( 'UserSettings', 'MaxNumberOfFailedLogin' ) : '0'; 00939 return $maxNumberOfFailedLogin; 00940 } 00941 00942 /* 00943 \static 00944 Returns true if the user can login 00945 If user has number of failed login attempts more than eZUser::maxNumberOfFailedLogin() 00946 and user is not trusted 00947 the user will not be allowed to login. 00948 */ 00949 static function isEnabledAfterFailedLogin( $userID, $ignoreTrusted = false ) 00950 { 00951 if ( !is_numeric( $userID ) ) 00952 return true; 00953 00954 $userObject = eZUser::fetch( $userID ); 00955 if ( !$userObject ) 00956 return true; 00957 00958 $trustedUser = eZUser::isTrusted(); 00959 // If user is trusted we should stop processing 00960 if ( $trustedUser and !$ignoreTrusted ) 00961 return true; 00962 00963 $maxNumberOfFailedLogin = eZUser::maxNumberOfFailedLogin(); 00964 00965 if ( $maxNumberOfFailedLogin == '0' ) 00966 return true; 00967 00968 $failedLoginAttempts = $userObject->failedLoginAttempts(); 00969 if ( $failedLoginAttempts > $maxNumberOfFailedLogin ) 00970 return false; 00971 00972 return true; 00973 } 00974 00975 /** 00976 * Makes sure the $user is set as the currently logged in user by 00977 * updating the session and setting the necessary global variables. 00978 * 00979 * All login handlers should use this function to ensure that the process 00980 * is executed properly. 00981 * 00982 * @access private 00983 * 00984 * @param eZUser $user User 00985 * @param int $userID User ID 00986 * @param int $flags Optional flag that can be set to: 00987 * eZUser::NO_SESSION_REGENERATE to avoid session to be regenerated 00988 */ 00989 static function setCurrentlyLoggedInUser( $user, $userID, $flags = 0 ) 00990 { 00991 $GLOBALS["eZUserGlobalInstance_$userID"] = $user; 00992 // Set/overwrite the global user, this will be accessed from 00993 // instance() when there is no ID passed to the function. 00994 $GLOBALS["eZUserGlobalInstance_"] = $user; 00995 eZSession::setUserID( $userID ); 00996 00997 if ( !( $flags & self::NO_SESSION_REGENERATE) ) 00998 eZSession::regenerate(); 00999 01000 eZSession::set( 'eZUserLoggedInID', $userID ); 01001 self::cleanup(); 01002 } 01003 01004 /*! 01005 \virtual 01006 Used by login handler to clean up session variables 01007 */ 01008 function sessionCleanup() 01009 { 01010 } 01011 01012 /** 01013 * Cleanup user related session values, for use by login / logout code 01014 * 01015 * @internal 01016 */ 01017 static function cleanup() 01018 { 01019 $http = eZHTTPTool::instance(); 01020 01021 $http->removeSessionVariable( 'CanInstantiateClassList' ); 01022 $http->removeSessionVariable( 'ClassesCachedForUser' ); 01023 01024 // Note: This must be done more generic with an internal 01025 // callback system. 01026 eZPreferences::sessionCleanup(); 01027 } 01028 01029 /*! 01030 \return logs in the current user object 01031 */ 01032 function loginCurrent() 01033 { 01034 self::setCurrentlyLoggedInUser( $this, $this->ContentObjectID ); 01035 } 01036 01037 /*! 01038 \static 01039 Logs out the current user 01040 */ 01041 static function logoutCurrent() 01042 { 01043 $http = eZHTTPTool::instance(); 01044 $id = false; 01045 $GLOBALS["eZUserGlobalInstance_$id"] = false; 01046 $contentObjectID = $http->sessionVariable( 'eZUserLoggedInID' ); 01047 01048 // reset session data 01049 $newUserID = self::anonymousId(); 01050 eZSession::setUserID( $newUserID ); 01051 $http->setSessionVariable( 'eZUserLoggedInID', $newUserID ); 01052 01053 // Clear current basket if necessary 01054 $db = eZDB::instance(); 01055 $db->begin(); 01056 eZBasket::cleanupCurrentBasket(); 01057 $db->commit(); 01058 01059 if ( $contentObjectID ) 01060 self::cleanup(); 01061 01062 // give user new session id 01063 eZSession::regenerate(); 01064 01065 // set the property used to prevent SSO from running again 01066 self::$userHasLoggedOut = true; 01067 } 01068 01069 /** 01070 * Returns a shared instance of the eZUser class pr $id value. 01071 * If user can not be fetched, then anonymous user is returned and 01072 * a warning trown, if anonymous user can not be fetched, then NoUser 01073 * is returned and another warning is thrown. 01074 * 01075 * @param int|false $id On false: Gets current user id from session 01076 * or from {@link eZUser::anonymousId()} if not set. 01077 * @return eZUser 01078 */ 01079 static function instance( $id = false ) 01080 { 01081 if ( !empty( $GLOBALS["eZUserGlobalInstance_$id"] ) ) 01082 { 01083 return $GLOBALS["eZUserGlobalInstance_$id"]; 01084 } 01085 01086 $userId = $id; 01087 $currentUser = null; 01088 $http = eZHTTPTool::instance(); 01089 $anonymousUserID = self::anonymousId(); 01090 $sessionHasStarted = eZSession::hasStarted(); 01091 // If not specified get the current user 01092 if ( $userId === false ) 01093 { 01094 if ( $sessionHasStarted ) 01095 { 01096 $userId = $http->sessionVariable( 'eZUserLoggedInID' ); 01097 if ( !is_numeric( $userId ) ) 01098 { 01099 $userId = $anonymousUserID; 01100 eZSession::setUserID( $userId ); 01101 $http->setSessionVariable( 'eZUserLoggedInID', $userId ); 01102 } 01103 } 01104 else 01105 { 01106 $userId = $anonymousUserID; 01107 eZSession::setUserID( $userId ); 01108 } 01109 } 01110 01111 // Check user cache (this effectivly fetches user from cache) 01112 // user not found if !isset( isset( $userCache['info'][$userId] ) ) 01113 $userCache = self::getUserCacheByUserId( $userId ); 01114 if ( isset( $userCache['info'][$userId] ) ) 01115 { 01116 $userArray = $userCache['info'][$userId]; 01117 if ( is_numeric( $userArray['contentobject_id'] ) ) 01118 { 01119 $currentUser = new eZUser( $userArray ); 01120 $currentUser->setUserCache( $userCache ); 01121 } 01122 } 01123 01124 $ini = eZINI::instance(); 01125 // Check if: 01126 // - the user has not logged out, 01127 // - the user is not logged in, 01128 // - and if a automatic single sign on plugin is enabled. 01129 if ( !self::$userHasLoggedOut and is_object( $currentUser ) and !$currentUser->isLoggedIn() ) 01130 { 01131 $ssoHandlerArray = $ini->variable( 'UserSettings', 'SingleSignOnHandlerArray' ); 01132 if ( !empty( $ssoHandlerArray ) ) 01133 { 01134 $ssoUser = false; 01135 foreach ( $ssoHandlerArray as $ssoHandler ) 01136 { 01137 $className = 'eZ' . $ssoHandler . 'SSOHandler'; 01138 if( class_exists( $className ) ) 01139 { 01140 $impl = new $className(); 01141 $ssoUser = $impl->handleSSOLogin(); 01142 // If a user was found via SSO, then use it 01143 if ( $ssoUser !== false ) 01144 { 01145 $currentUser = $ssoUser; 01146 $userId = $currentUser->attribute( 'contentobject_id' ); 01147 01148 $userInfo = array(); 01149 $userInfo[$userId] = array( 01150 'contentobject_id' => $userId, 01151 'login' => $currentUser->attribute( 'login' ), 01152 'email' => $currentUser->attribute( 'email' ), 01153 'password_hash' => $currentUser->attribute( 'password_hash' ), 01154 'password_hash_type' => $currentUser->attribute( 'password_hash_type' ) 01155 ); 01156 eZSession::setUserID( $userId ); 01157 $http->setSessionVariable( 'eZUserLoggedInID', $userId ); 01158 01159 eZUser::updateLastVisit( $userId ); 01160 eZUser::setCurrentlyLoggedInUser( $currentUser, $userId ); 01161 eZHTTPTool::redirect( eZSys::wwwDir() . eZSys::indexFile( false ) . eZSys::requestURI(), array(), 302 ); 01162 eZExecution::cleanExit(); 01163 } 01164 } 01165 else 01166 { 01167 eZDebug::writeError( "Undefined ssoHandler class: $className", __METHOD__ ); 01168 } 01169 } 01170 } 01171 } 01172 01173 if ( $userId <> $anonymousUserID ) 01174 { 01175 $sessionInactivityTimeout = $ini->variable( 'Session', 'ActivityTimeout' ); 01176 if ( !isset( $GLOBALS['eZSessionIdleTime'] ) ) 01177 { 01178 eZUser::updateLastVisit( $userId ); 01179 } 01180 else 01181 { 01182 $sessionIdle = $GLOBALS['eZSessionIdleTime']; 01183 if ( $sessionIdle > $sessionInactivityTimeout ) 01184 { 01185 eZUser::updateLastVisit( $userId ); 01186 } 01187 } 01188 } 01189 01190 if ( !$currentUser ) 01191 { 01192 $currentUser = eZUser::fetch( self::anonymousId() ); 01193 eZDebug::writeWarning( 'User not found, returning anonymous' ); 01194 } 01195 01196 if ( !$currentUser ) 01197 { 01198 $currentUser = new eZUser( array( 'id' => -1, 'login' => 'NoUser' ) ); 01199 eZDebug::writeWarning( 'Anonymous user not found, returning NoUser' ); 01200 } 01201 01202 $GLOBALS["eZUserGlobalInstance_$id"] = $currentUser; 01203 return $currentUser; 01204 } 01205 01206 /** 01207 * Get User cache from cache file 01208 * 01209 * @since 4.4 01210 * @return array( 'info' => array, 'groups' => array, 'roles' => array, 'role_limitations' => array, 'access_array' => array) 01211 */ 01212 public function getUserCache() 01213 { 01214 if ( $this->UserCache === null ) 01215 { 01216 $this->setUserCache( self::getUserCacheByUserId( $this->ContentObjectID ) ); 01217 } 01218 return $this->UserCache; 01219 } 01220 01221 /** 01222 * Delete User cache from locale var and cache file for current user. 01223 * 01224 * @since 4.4 01225 */ 01226 public function purgeUserCache() 01227 { 01228 $this->UserCache = null; 01229 self::purgeUserCacheByUserId( $this->ContentObjectID ); 01230 } 01231 01232 /** 01233 * Set User cache from cache file 01234 * Needs to be in excact same format as {@link eZUser::getUserCache()}! 01235 * 01236 * @since 4.4 01237 * @param array $userCache 01238 */ 01239 public function setUserCache( array $userCache ) 01240 { 01241 $this->UserCache = $userCache; 01242 } 01243 01244 /** 01245 * Delete User cache from cache file for Anonymous user(usefull for sessionless users) 01246 * 01247 * @since 4.4 01248 * @see eZUser::purgeUserCacheByUserId() 01249 */ 01250 static public function purgeUserCacheByAnonymousId() 01251 { 01252 self::purgeUserCacheByUserId( self::anonymousId() ); 01253 } 01254 01255 /** 01256 * Delete User cache pr user 01257 * 01258 * @since 4.4 01259 * @param int $userId 01260 */ 01261 static public function purgeUserCacheByUserId( $userId ) 01262 { 01263 $cacheFilePath = eZUser::getCacheDir( $userId ). "/user-data-{$userId}.cache.php" ; 01264 eZClusterFileHandler::instance()->fileDelete( $cacheFilePath ); 01265 } 01266 01267 /** 01268 * Get User cache from cache file for Anonymous user(usefull for sessionless users) 01269 * 01270 * @since 4.4 01271 * @see eZUser::getUserCacheByUserId() 01272 * @return array 01273 */ 01274 static public function getUserCacheByAnonymousId() 01275 { 01276 return self::getUserCacheByUserId( self::anonymousId() ); 01277 } 01278 01279 /** 01280 * Get User cache from cache file (usefull for sessionless users) 01281 * 01282 * @since 4.4 01283 * @see eZUser::getUserCache() 01284 * @param int $userId 01285 * @return array 01286 */ 01287 static protected function getUserCacheByUserId( $userId ) 01288 { 01289 $cacheFilePath = eZUser::getCacheDir( $userId ). "/user-data-{$userId}.cache.php" ; 01290 $cacheFile = eZClusterFileHandler::instance( $cacheFilePath ); 01291 return $cacheFile->processCache( array( 'eZUser', 'retrieveUserCacheFromFile' ), 01292 array( 'eZUser', 'generateUserCacheForFile' ), 01293 null, 01294 self::userInfoExpiry(), 01295 $userId ); 01296 } 01297 01298 /** 01299 * Callback which fetches user cache from local file. 01300 * 01301 * @internal 01302 * @since 4.4 01303 * @see eZUser::getUserCacheByUserId() 01304 */ 01305 static function retrieveUserCacheFromFile( $filePath, $mtime, $userId ) 01306 { 01307 return include( $filePath ); 01308 } 01309 01310 /** 01311 * Callback which generates user cache for user 01312 * 01313 * @internal 01314 * @since 4.4 01315 * @see eZUser::getUserCacheByUserId() 01316 */ 01317 static function generateUserCacheForFile( $filePath, $userId ) 01318 { 01319 $data = array( 'info' => array(), 01320 'groups' => array(), 01321 'roles' => array(), 01322 'role_limitations' => array(), 01323 'access_array' => array(), 01324 'discount_rules' => array() ); 01325 $user = eZUser::fetch( $userId ); 01326 if ( $user instanceOf eZUser ) 01327 { 01328 // user info (session: eZUserInfoCache) 01329 $data['info'][$userId] = array( 'contentobject_id' => $user->attribute( 'contentobject_id' ), 01330 'login' => $user->attribute( 'login' ), 01331 'email' => $user->attribute( 'email' ), 01332 'password_hash' => $user->attribute( 'password_hash' ), 01333 'password_hash_type' => $user->attribute( 'password_hash_type' ) ); 01334 01335 // user groups list (session: eZUserGroupsCache) 01336 $groups = $user->generateGroupIdList(); 01337 $data['groups'] = $groups; 01338 01339 // role list (session: eZRoleIDList) 01340 $groups[] = $userId; 01341 $data['roles'] = eZRole::fetchIDListByUser( $groups ); 01342 01343 // role limitation list (session: eZRoleLimitationValueList) 01344 $limitList = $user->limitList( false ); 01345 foreach ( $limitList as $limit ) 01346 { 01347 $data['role_limitations'][] = $limit['limit_value']; 01348 } 01349 01350 // access array (session: AccessArray) 01351 $data['access_array'] = $user->generateAccessArray(); 01352 01353 // discount rules (session: eZUserDiscountRules<userId>) 01354 $data['discount_rules'] = eZUserDiscountRule::generateIDListByUserID( $userId ); 01355 } 01356 return array( 'content' => $data, 01357 'scope' => 'user-info-cache', 01358 'datatype' => 'php', 01359 'store' => true ); 01360 } 01361 01362 /*! 01363 Updates the user's last visit timestamp 01364 Optionally updates user login count by setting $updateLoginCount to true 01365 */ 01366 static function updateLastVisit( $userID, $updateLoginCount = false ) 01367 { 01368 if ( isset( $GLOBALS['eZUserUpdatedLastVisit'] ) ) 01369 return; 01370 $db = eZDB::instance(); 01371 $userID = (int) $userID; 01372 $userVisitArray = $db->arrayQuery( "SELECT 1 FROM ezuservisit WHERE user_id=$userID" ); 01373 $time = time(); 01374 01375 if ( isset( $userVisitArray[0] ) ) 01376 { 01377 $loginCountSQL = $updateLoginCount ? ', login_count=login_count+1' : ''; 01378 $db->query( "UPDATE ezuservisit SET last_visit_timestamp=current_visit_timestamp, current_visit_timestamp=$time$loginCountSQL WHERE user_id=$userID" ); 01379 } 01380 else 01381 { 01382 $intialLoginCount = $updateLoginCount ? 1 : 0; 01383 $db->query( "INSERT INTO ezuservisit ( current_visit_timestamp, last_visit_timestamp, user_id, login_count ) VALUES ( $time, $time, $userID, $intialLoginCount )" ); 01384 } 01385 $GLOBALS['eZUserUpdatedLastVisit'] = true; 01386 } 01387 01388 /*! 01389 Returns the last visit timestamp to the current user. 01390 */ 01391 function lastVisit() 01392 { 01393 $db = eZDB::instance(); 01394 01395 $userVisitArray = $db->arrayQuery( "SELECT last_visit_timestamp FROM ezuservisit WHERE user_id=$this->ContentObjectID" ); 01396 if ( isset( $userVisitArray[0] ) ) 01397 { 01398 return $userVisitArray[0]['last_visit_timestamp']; 01399 } 01400 else 01401 { 01402 return time(); 01403 } 01404 } 01405 01406 /** 01407 * Returns the login count for the current user. 01408 * 01409 * @since Version 4.1 01410 * @return int Login count for current user. 01411 */ 01412 function loginCount() 01413 { 01414 $db = eZDB::instance(); 01415 $userVisitArray = $db->arrayQuery( "SELECT login_count FROM ezuservisit WHERE user_id=$this->ContentObjectID" ); 01416 if ( isset( $userVisitArray[0] ) ) 01417 { 01418 return $userVisitArray[0]['login_count']; 01419 } 01420 else 01421 { 01422 return 0; 01423 } 01424 } 01425 01426 /*! 01427 If \a $value is false will increase the user's number of failed login attempts 01428 otherwise failed_login_attempts will be updated by $value. 01429 \a $setByForce if true checking for trusting or max number of failed login attempts will be ignored. 01430 */ 01431 static function setFailedLoginAttempts( $userID, $value = false, $setByForce = false ) 01432 { 01433 $trustedUser = eZUser::isTrusted(); 01434 // If user is trusted we should stop processing 01435 if ( $trustedUser and !$setByForce ) 01436 return true; 01437 01438 $maxNumberOfFailedLogin = eZUser::maxNumberOfFailedLogin(); 01439 01440 if ( $maxNumberOfFailedLogin == '0' and !$setByForce ) 01441 return true; 01442 01443 $userID = (int) $userID; 01444 $userObject = eZUser::fetch( $userID ); 01445 if ( !$userObject ) 01446 return true; 01447 01448 $isEnabled = $userObject->isEnabled(); 01449 // If current user is disabled we should not continue 01450 if ( !$isEnabled and !$setByForce ) 01451 return true; 01452 01453 $db = eZDB::instance(); 01454 $db->begin(); 01455 01456 $userVisitArray = $db->arrayQuery( "SELECT 1 FROM ezuservisit WHERE user_id=$userID" ); 01457 01458 if ( isset( $userVisitArray[0] ) ) 01459 { 01460 if ( $value === false ) 01461 { 01462 $failedLoginAttempts = $userObject->failedLoginAttempts(); 01463 $failedLoginAttempts += 1; 01464 } 01465 else 01466 $failedLoginAttempts = (int) $value; 01467 01468 $db->query( "UPDATE ezuservisit SET failed_login_attempts=$failedLoginAttempts WHERE user_id=$userID" ); 01469 } 01470 else 01471 { 01472 if ( $value === false ) 01473 { 01474 $failedLoginAttempts = 1; 01475 } 01476 else 01477 $failedLoginAttempts = (int) $value; 01478 01479 $db->query( "INSERT INTO ezuservisit ( failed_login_attempts, user_id ) VALUES ( $failedLoginAttempts, $userID )" ); 01480 } 01481 $db->commit(); 01482 01483 eZContentCacheManager::clearContentCacheIfNeeded( $userID ); 01484 eZContentCacheManager::generateObjectViewCache( $userID ); 01485 } 01486 01487 /*! 01488 Returns the current user's number of failed login attempts. 01489 */ 01490 function failedLoginAttempts() 01491 { 01492 return eZUser::failedLoginAttemptsByUserID( $this->attribute( 'contentobject_id' ) ); 01493 } 01494 01495 /*! 01496 Returns the current user's number of failed login attempts. 01497 */ 01498 static function failedLoginAttemptsByUserID( $userID ) 01499 { 01500 $db = eZDB::instance(); 01501 $contentObjectID = (int) $userID; 01502 01503 $userVisitArray = $db->arrayQuery( "SELECT failed_login_attempts FROM ezuservisit WHERE user_id=$contentObjectID" ); 01504 01505 $failedLoginAttempts = isset( $userVisitArray[0] ) ? $userVisitArray[0]['failed_login_attempts'] : 0; 01506 return $failedLoginAttempts; 01507 } 01508 01509 /*! 01510 \return \c true if the user is locked (is enabled after failed login) and can be logged on the site. 01511 */ 01512 function isLocked() 01513 { 01514 $userID = $this->attribute( 'contentobject_id' ); 01515 $isNotLocked = eZUser::isEnabledAfterFailedLogin( $userID, true ); 01516 return !$isNotLocked; 01517 } 01518 01519 /*! 01520 \return \c true if the user is enabled and can be used on the site. 01521 */ 01522 function isEnabled() 01523 { 01524 if ( $this == eZUser::currentUser() ) 01525 { 01526 return true; 01527 } 01528 01529 $setting = eZUserSetting::fetch( $this->attribute( 'contentobject_id' ) ); 01530 if ( $setting and !$setting->attribute( 'is_enabled' ) ) 01531 { 01532 return false; 01533 } 01534 return true; 01535 } 01536 01537 /*! 01538 \return \c true if the user is the anonymous user. 01539 */ 01540 function isAnonymous() 01541 { 01542 if ( $this->attribute( 'contentobject_id' ) != self::anonymousId() ) 01543 { 01544 return false; 01545 } 01546 return true; 01547 } 01548 01549 /*! 01550 \static 01551 Returns the currently logged in user. 01552 */ 01553 static function currentUser() 01554 { 01555 $user = eZUser::instance(); 01556 return $user; 01557 } 01558 01559 /*! 01560 \static 01561 Returns the ID of the currently logged in user. 01562 */ 01563 static function currentUserID() 01564 { 01565 $user = eZUser::instance(); 01566 if ( !$user ) 01567 return 0; 01568 return $user->attribute( 'contentobject_id' ); 01569 } 01570 01571 /*! 01572 \static 01573 Creates a hash out of \a $user, \a $password and \a $site according to the type \a $type. 01574 \return true if the generated hash is equal to the supplied hash \a $hash. 01575 */ 01576 static function authenticateHash( $user, $password, $site, $type, $hash ) 01577 { 01578 return eZUser::createHash( $user, $password, $site, $type, $hash ) === (string) $hash; 01579 } 01580 01581 /*! 01582 \static 01583 \return an array with characters which are allowed in password. 01584 */ 01585 static function passwordCharacterTable() 01586 { 01587 if ( !empty( $GLOBALS['eZUserPasswordCharacterTable'] ) ) 01588 { 01589 return $GLOBALS['eZUserPasswordCharacterTable']; 01590 } 01591 $table = array_merge( range( 'a', 'z' ), range( 'A', 'Z' ), range( 0, 9 ) ); 01592 01593 $ini = eZINI::instance(); 01594 if ( $ini->variable( 'UserSettings', 'UseSpecialCharacters' ) == 'true' ) 01595 { 01596 $specialCharacters = '!#%&{[]}+?;:*'; 01597 $table = array_merge( $table, preg_split( '//', $specialCharacters, -1, PREG_SPLIT_NO_EMPTY ) ); 01598 } 01599 // Remove some characters that are too similar visually 01600 $table = array_diff( $table, array( 'I', 'l', 'o', 'O', '0' ) ); 01601 $tableTmp = $table; 01602 $table = array(); 01603 foreach ( $tableTmp as $item ) 01604 { 01605 $table[] = $item; 01606 } 01607 01608 return $GLOBALS['eZUserPasswordCharacterTable'] = $table; 01609 } 01610 01611 /*! 01612 Checks if the supplied content object is a user object ( contains ezuser datatype ) 01613 01614 \param ContentObject 01615 01616 \return true or false 01617 */ 01618 static function isUserObject( $contentObject ) 01619 { 01620 if ( !$contentObject ) 01621 { 01622 return false; 01623 } 01624 01625 eZDataType::loadAndRegisterType( 'ezuser' ); 01626 01627 $contentClass = $contentObject->attribute( 'content_class' ); 01628 $classAttributeList = $contentClass->fetchAttributes(); 01629 foreach( $classAttributeList as $classAttribute ) 01630 { 01631 if ( $classAttribute->attribute( 'data_type_string' ) == eZUserType::DATA_TYPE_STRING ) 01632 return true; 01633 } 01634 01635 return false; 01636 } 01637 01638 /*! 01639 \static 01640 Creates a password with number of characters equal to \a $passwordLength and returns it. 01641 If you want pass a value in \a $seed it will be used as basis for the password, if not 01642 it will use the current time value as seed. 01643 \note If \a $passwordLength exceeds 16 it will need to generate new seed for the remaining 01644 characters. 01645 */ 01646 static function createPassword( $passwordLength, $seed = false ) 01647 { 01648 $chars = 0; 01649 $password = ''; 01650 if ( $passwordLength < 1 ) 01651 $passwordLength = 1; 01652 $decimal = 0; 01653 while ( $chars < $passwordLength ) 01654 { 01655 if ( $seed == false ) 01656 $seed = time() . ":" . mt_rand(); 01657 $text = md5( $seed ); 01658 $characterTable = eZUser::passwordCharacterTable(); 01659 $tableCount = count( $characterTable ); 01660 for ( $i = 0; ( $chars < $passwordLength ) and $i < 32; ++$chars, $i += 2 ) 01661 { 01662 $decimal += hexdec( substr( $text, $i, 2 ) ); 01663 $index = ( $decimal % $tableCount ); 01664 $character = $characterTable[$index]; 01665 $password .= $character; 01666 } 01667 $seed = false; 01668 } 01669 return $password; 01670 } 01671 01672 /*! 01673 \static 01674 Will create a hash of the given string. This is used to store the passwords in the database. 01675 */ 01676 static function createHash( $user, $password, $site, $type, $hash = false ) 01677 { 01678 $str = ''; 01679 if( $type == self::PASSWORD_HASH_MD5_USER ) 01680 { 01681 $str = md5( "$user\n$password" ); 01682 } 01683 else if ( $type == self::PASSWORD_HASH_MD5_SITE ) 01684 { 01685 $str = md5( "$user\n$password\n$site" ); 01686 } 01687 else if ( $type == self::PASSWORD_HASH_MYSQL ) 01688 { 01689 $db = eZDB::instance(); 01690 $hash = $db->escapeString( $password ); 01691 01692 $str = $db->arrayQuery( "SELECT PASSWORD( '$hash' )" ); 01693 $hashes = array_values( $str[0] ); 01694 $str = $hashes[0]; 01695 } 01696 else if ( $type == self::PASSWORD_HASH_PLAINTEXT ) 01697 { 01698 $str = $password; 01699 } 01700 else if ( $type == self::PASSWORD_HASH_CRYPT ) 01701 { 01702 if ( $hash ) 01703 { 01704 $str = crypt( $password, $hash ); 01705 } 01706 else 01707 { 01708 $str = crypt( $password ); 01709 } 01710 } 01711 else // self::PASSWORD_HASH_MD5_PASSWORD 01712 { 01713 $str = md5( $password ); 01714 } 01715 eZDebugSetting::writeDebug( 'kernel-user', $str, "ezuser($type)" ); 01716 return $str; 01717 } 01718 01719 /*! 01720 Check if user has got access to the specified module and function 01721 01722 \param module name 01723 \param funtion name 01724 01725 \return Array containg result. 01726 Array elements : 'accessWord', yes - access allowed 01727 no - access denied 01728 limited - access array describing access included 01729 'policies', array containing the policy limitations 01730 'accessList', array describing missing access rights 01731 */ 01732 function hasAccessTo( $module, $function = false ) 01733 { 01734 $accessArray = $this->accessArray(); 01735 01736 $functionArray = array(); 01737 if ( isset( $accessArray['*']['*'] ) ) 01738 { 01739 $functionArray = $accessArray['*']['*']; 01740 } 01741 if ( isset( $accessArray[$module] ) ) 01742 { 01743 if ( isset( $accessArray[$module]['*'] ) ) 01744 { 01745 $functionArray = array_merge_recursive( $functionArray, $accessArray[$module]['*'] ); 01746 } 01747 if ( $function and isset( $accessArray[$module][$function] ) and $function != '*' ) 01748 { 01749 $functionArray = array_merge_recursive( $functionArray, $accessArray[$module][$function] ); 01750 } 01751 } 01752 01753 if ( !$functionArray ) 01754 { 01755 return array( 'accessWord' => 'no', 01756 'accessList' => array( 'FunctionRequired' => array ( 'Module' => $module, 01757 'Function' => $function, 01758 'ClassID' => '', 01759 'MainNodeID' => '' ), 01760 'PolicyList' => array() ) 01761 ); 01762 } 01763 01764 if ( isset( $functionArray['*'] ) && 01765 ( $functionArray['*'] == '*' || in_array( '*', $functionArray['*'] ) ) ) 01766 { 01767 return array( 'accessWord' => 'yes' ); 01768 } 01769 01770 return array( 'accessWord' => 'limited', 'policies' => $functionArray ); 01771 } 01772 01773 /** 01774 * Returns either cached or newly generated accessArray for the user depending on 01775 * site.ini\[RoleSettings]\EnableCaching setting 01776 * 01777 * @access private 01778 * @return array 01779 */ 01780 function accessArray() 01781 { 01782 if ( !isset( $this->AccessArray ) ) 01783 { 01784 if ( eZINI::instance()->variable( 'RoleSettings', 'EnableCaching' ) === 'true' ) 01785 { 01786 $userCache = $this->getUserCache(); 01787 $this->AccessArray = $userCache['access_array']; 01788 } 01789 else 01790 { 01791 // if role caching is disabled then generate access array on-the-fly. 01792 $this->AccessArray = $this->generateAccessArray(); 01793 } 01794 } 01795 return $this->AccessArray; 01796 } 01797 01798 /** 01799 * Generates the accessArray for the user (for $this). 01800 * This function is uncached, and used as basis for user cache callback. 01801 * 01802 * @internal 01803 * @return array 01804 */ 01805 function generateAccessArray() 01806 { 01807 $idList = $this->generateGroupIdList(); 01808 $idList[] = $this->attribute( 'contentobject_id' ); 01809 01810 return eZRole::accessArrayByUserID( $idList ); 01811 } 01812 01813 /*! 01814 \private 01815 \static 01816 Callback which figures out global expiry and returns it. 01817 */ 01818 static protected function userInfoExpiry() 01819 { 01820 /* Figure out when the last update was done */ 01821 eZExpiryHandler::registerShutdownFunction(); 01822 $handler = eZExpiryHandler::instance(); 01823 if ( $handler->hasTimestamp( 'user-info-cache' ) ) 01824 { 01825 $expiredTimestamp = $handler->timestamp( 'user-info-cache' ); 01826 } 01827 else 01828 { 01829 $expiredTimestamp = time(); 01830 $handler->setTimestamp( 'user-info-cache', $expiredTimestamp ); 01831 } 01832 01833 return $expiredTimestamp; 01834 } 01835 01836 /* 01837 Returns list of sections which are allowed to assign to the given content object by the user. 01838 */ 01839 function canAssignToObjectSectionList( $contentObject ) 01840 { 01841 $access = $this->hasAccessTo( 'section', 'assign' ); 01842 01843 if ( $access['accessWord'] == 'yes' ) 01844 { 01845 return array( '*' ); 01846 } 01847 else if ( $access['accessWord'] == 'limited' ) 01848 { 01849 $userID = $this->attribute( 'contentobject_id' ); 01850 $classID = $contentObject->attribute( 'contentclass_id' ); 01851 $ownerID = $contentObject->attribute( 'owner_id' ); 01852 $sectionID = $contentObject->attribute( 'section_id' ); 01853 01854 $allowedSectionIDList = array(); 01855 foreach ( $access['policies'] as $policy ) 01856 { 01857 if ( ( isset( $policy['Class'] ) and !in_array( $classID, $policy['Class'] ) ) or 01858 ( isset( $policy['Owner'] ) and in_array( 1, $policy['Owner'] ) and $userID != $ownerID ) or 01859 ( isset( $policy['Section'] ) and !in_array( $sectionID, $policy['Section'] ) ) ) 01860 { 01861 continue; 01862 } 01863 if ( isset( $policy['NewSection'] ) && !empty( $policy['NewSection'] ) ) 01864 { 01865 $allowedSectionIDList = array_merge( $allowedSectionIDList, $policy['NewSection'] ); 01866 } 01867 else 01868 { 01869 return array( '*' ); 01870 } 01871 } 01872 $allowedSectionIDList = array_unique( $allowedSectionIDList ); 01873 return $allowedSectionIDList; 01874 } 01875 return array(); 01876 } 01877 01878 /* 01879 Checks whether user can assign the section to the given content object or not. 01880 */ 01881 function canAssignSectionToObject( $checkSectionID, $contentObject ) 01882 { 01883 $access = $this->hasAccessTo( 'section', 'assign' ); 01884 01885 if ( $access['accessWord'] == 'yes' ) 01886 { 01887 return true; 01888 } 01889 else if ( $access['accessWord'] == 'limited' ) 01890 { 01891 $hasSubtreeLimitation = false; 01892 $subtreeLimitationResult = false; 01893 01894 // Trying first to discover if a subtree limitation exists. 01895 // Only the main node of the object is considered! 01896 foreach ( $access['policies'] as $policy ) 01897 { 01898 if ( isset( $policy['User_Subtree'] ) ) 01899 { 01900 $hasSubtreeLimitation = true; 01901 $nodePath = $contentObject->mainNode()->PathString; 01902 01903 foreach ( $policy['User_Subtree'] as $path ) 01904 { 01905 if ( strpos( $nodePath, $path ) !== false ) 01906 { 01907 $subtreeLimitationResult = true; 01908 } 01909 } 01910 continue; 01911 } 01912 } 01913 01914 // If a subtree limitation exists and none of the path corresponds then the user have not enough rights. 01915 if ( $hasSubtreeLimitation && !$subtreeLimitationResult ) 01916 { 01917 return false; 01918 } 01919 01920 unset( $hasSubtreeLimitation, $subtreeLimitationResult, $nodePath ); 01921 01922 $userID = $this->attribute( 'contentobject_id' ); 01923 $classID = $contentObject->attribute( 'contentclass_id' ); 01924 $ownerID = $contentObject->attribute( 'owner_id' ); 01925 $sectionID = $contentObject->attribute( 'section_id' ); 01926 01927 foreach ( $access['policies'] as $policy ) 01928 { 01929 if ( ( isset( $policy['Class'] ) and !in_array( $classID, $policy['Class'] ) ) or 01930 ( isset( $policy['Owner'] ) and in_array( 1, $policy['Owner'] ) and $userID != $ownerID ) or 01931 ( isset( $policy['Section'] ) and !in_array( $sectionID, $policy['Section'] ) ) ) 01932 { 01933 continue; 01934 } 01935 if ( isset( $policy['NewSection'] ) ) 01936 { 01937 if ( is_array( $policy['NewSection'] ) and in_array( $checkSectionID, $policy['NewSection'] ) ) 01938 { 01939 return true; 01940 } 01941 } 01942 else 01943 { 01944 return true; 01945 } 01946 } 01947 } 01948 return false; 01949 } 01950 01951 /* 01952 Checks whether user has privileges to assign the section or not at all. 01953 */ 01954 function canAssignSection( $checkSectionID ) 01955 { 01956 $access = $this->hasAccessTo( 'section', 'assign' ); 01957 01958 if ( $access['accessWord'] == 'yes' ) 01959 { 01960 return true; 01961 } 01962 else if ( $access['accessWord'] == 'limited' ) 01963 { 01964 foreach ( $access['policies'] as $policy ) 01965 { 01966 if ( isset( $policy['NewSection'] ) ) 01967 { 01968 if ( in_array( $checkSectionID, $policy['NewSection'] ) ) 01969 { 01970 return true; 01971 } 01972 } 01973 else 01974 { 01975 return true; 01976 } 01977 } 01978 } 01979 return false; 01980 } 01981 01982 /* 01983 Returns list of sections allowed to assign for the user. 01984 */ 01985 function canAssignSectionList() 01986 { 01987 $access = $this->hasAccessTo( 'section', 'assign' ); 01988 01989 if ( $access['accessWord'] == 'yes' ) 01990 { 01991 return array( '*' ); 01992 } 01993 else if ( $access['accessWord'] == 'limited' ) 01994 { 01995 $allowedSectionIDList = array(); 01996 foreach ( $access['policies'] as $policy ) 01997 { 01998 if ( isset( $policy['NewSection'] ) ) 01999 { 02000 if ( is_array( $policy['NewSection'] ) && !empty( $policy['NewSection'] ) ) 02001 { 02002 $allowedSectionIDList = array_merge( $allowedSectionIDList, $policy['NewSection'] ); 02003 } 02004 } 02005 else 02006 { 02007 return array( '*' ); 02008 } 02009 } 02010 $allowedSectionIDList = array_unique( $allowedSectionIDList ); 02011 return $allowedSectionIDList; 02012 } 02013 return array(); 02014 } 02015 02016 /* 02017 Returns list of classes allowed to assign to the given section for the user. 02018 */ 02019 function canAssignSectionToClassList( $checkSectionID ) 02020 { 02021 $access = $this->hasAccessTo( 'section', 'assign' ); 02022 02023 if ( $access['accessWord'] == 'yes' ) 02024 { 02025 return array( '*' ); 02026 } 02027 else if ( $access['accessWord'] == 'limited' ) 02028 { 02029 $allowedClassList = array(); 02030 foreach ( $access['policies'] as $policy ) 02031 { 02032 if ( !isset( $policy['NewSection'] ) or in_array( $checkSectionID, $policy['NewSection'] ) ) 02033 { 02034 if ( isset( $policy['Class'] ) ) 02035 { 02036 $allowedClassList = array_merge( $allowedClassList, $policy['Class'] ); 02037 } 02038 else 02039 { 02040 return array( '*' ); 02041 } 02042 } 02043 } 02044 02045 if ( !empty( $allowedClassList ) ) 02046 { 02047 // Now we are trying to fetch classes by collected ids list to return 02048 // class list consisting of existing classes's identifiers only. 02049 $allowedClassList = array_unique( $allowedClassList ); 02050 $classList = eZContentClass::fetchList( eZContentClass::VERSION_STATUS_DEFINED, false, false, null, null, $allowedClassList ); 02051 if ( is_array( $classList ) && !empty( $classList ) ) 02052 { 02053 $classIdentifierList = array(); 02054 foreach( $classList as $class ) 02055 { 02056 $classIdentifierList[] = $class['identifier']; 02057 } 02058 return $classIdentifierList; 02059 } 02060 } 02061 } 02062 return array(); 02063 } 02064 02065 /* 02066 Evaluates if $this user has access to the view $viewName based on its policy functions and 02067 checks if the assigned to the view functions expression is valid and handles errors if it is not. 02068 Returns true if access is allowed, false if access is denied. 02069 */ 02070 function hasAccessToView( $module, $viewName, &$params ) 02071 { 02072 $validView = false; 02073 $accessAllowed = false; 02074 if ( $module->singleFunction() ) 02075 { 02076 $info = $module->attribute( 'info' ); 02077 if ( isset( $info['function'] ) ) 02078 { 02079 $validView = true; 02080 if ( isset( $info['function']['functions'] ) && !empty( $info['function']['functions'] ) ) 02081 { 02082 $functions = $info['function']['functions']; 02083 } 02084 } 02085 } 02086 else 02087 { 02088 $views = $module->attribute( 'views' ); 02089 if ( isset( $views[$viewName] ) ) 02090 { 02091 $view = $views[$viewName]; 02092 $validView = true; 02093 if ( isset( $view['functions'] ) && !empty( $view['functions'] ) ) 02094 { 02095 $functions = $view['functions']; 02096 } 02097 } 02098 } 02099 02100 if ( $validView ) 02101 { 02102 if ( isset( $functions ) ) 02103 { 02104 if ( is_array( $functions ) ) 02105 { 02106 $funcExpression = false; 02107 $accessAllowed = true; 02108 foreach ( $functions as $function ) 02109 { 02110 if ( empty( $function ) ) 02111 { 02112 $funcExpression = false; 02113 $accessAllowed = false; 02114 break; 02115 } 02116 else if ( is_string( $function ) ) 02117 { 02118 if ( $funcExpression ) 02119 { 02120 $funcExpression .= ' and '; 02121 } 02122 $funcExpression .= '( ' . $function . ' )'; 02123 } 02124 } 02125 } 02126 else if ( is_string( $functions ) ) 02127 { 02128 $funcExpression = $functions; 02129 } 02130 else 02131 { 02132 $funcExpression = false; 02133 $accessAllowed = true; 02134 } 02135 02136 if ( $funcExpression ) 02137 { 02138 // Validate and evaluate functions expression. 02139 // Lets construct functions's expression ready for evaluating first. 02140 $pS = '/(?<=\b)'; 02141 $pE = '(?=\b)/'; 02142 02143 $moduleName = $module->attribute( 'name' ); 02144 $availableFunctions = $module->attribute( 'available_functions' ); 02145 if ( is_array( $availableFunctions ) and 02146 !empty( $availableFunctions ) ) 02147 { 02148 $pattern = $pS . '(' . implode( '|', array_keys( $availableFunctions ) ) . ')' . $pE; 02149 $matches = array(); 02150 if ( preg_match_all( $pattern, ' ' . $funcExpression . ' ', $matches ) > 0 ) 02151 { 02152 $patterns = array(); 02153 $replacements = array(); 02154 $matches = array_unique( $matches[1] ); 02155 foreach ( $matches as $match ) 02156 { 02157 if ( !isset( $replacements[$match] ) ) 02158 { 02159 $accessResult = $this->hasAccessTo( $moduleName, $match ); 02160 if ( $accessResult['accessWord'] == 'no' ) 02161 { 02162 $replacements[$match] = 'false'; 02163 $params['accessList'] = $accessResult['accessList']; 02164 } 02165 else 02166 { 02167 $replacements[$match] = 'true'; 02168 if ( $accessResult['accessWord'] == 'limited' ) 02169 { 02170 $params['Limitation'] = $accessResult['policies']; 02171 $GLOBALS['ezpolicylimitation_list'][$this->ContentObjectID][$moduleName][$match] = $params['Limitation']; 02172 } 02173 } 02174 $patterns[$match] = $pS . $match . $pE; 02175 } 02176 } 02177 $funcExpression = preg_replace( $patterns, $replacements, ' ' . $funcExpression . ' ' ); 02178 } 02179 } 02180 $funcExpressionForEval = $funcExpression; 02181 02182 // continue to validate expression 02183 $words = array(); 02184 $words[] = $pS . 'and' . $pE; 02185 $words[] = $pS . 'or' . $pE; 02186 $words[] = $pS . 'true' . $pE; 02187 $words[] = $pS . 'false' . $pE; 02188 $pS = '/(?<=[^&|])'; 02189 $pE = '(?=[^&|])/'; 02190 $words[] = $pS . '\|\|' . $pE; 02191 $words[] = $pS . '&&' . $pE; 02192 $words[] = '/[\(\)]/'; 02193 02194 $replacement = ''; 02195 $funcExpression = preg_replace( $words, $replacement, ' ' . $funcExpression . ' ' ); 02196 02197 $funcExpression = trim( $funcExpression ); 02198 02199 if ( empty( $funcExpression ) ) 02200 { 02201 // if expression is valid then evaluate value of the $functionsToEvaluate string 02202 ob_start(); 02203 $ret = eval( "\$accessAllowed = ( bool ) ( $funcExpressionForEval );" ); 02204 $buffer = ob_get_contents(); 02205 ob_end_clean(); 02206 02207 // if we get any error while evaluating then set result to false 02208 if ( !empty( $buffer ) or $ret === false ) 02209 { 02210 eZDebug::writeError( "There was an error while evaluating the policy functions value of the '$moduleName/$viewName' view. " . 02211 "Please check the '$moduleName/module.php' file." ); 02212 $accessAllowed = false; 02213 } 02214 } 02215 else 02216 { 02217 eZDebug::writeError( "There is a mistake in the functions array data of the '$moduleName/$viewName' view. " . 02218 "Please check the '$moduleName/module.php' file." ); 02219 $accessAllowed = false; 02220 } 02221 } 02222 } 02223 else 02224 { 02225 $moduleName = $module->attribute( 'name' ); 02226 $accessResult = $this->hasAccessTo( $moduleName ); 02227 if ( $accessResult['accessWord'] == 'no' ) 02228 { 02229 $params['accessList'] = $accessResult['accessList']; 02230 $accessAllowed = false; 02231 } 02232 else 02233 { 02234 $accessAllowed = true; 02235 if ( $accessResult['accessWord'] == 'limited' ) 02236 { 02237 $params['Limitation'] = $accessResult['policies']; 02238 $GLOBALS['ezpolicylimitation_list'][$this->ContentObjectID][$moduleName]['*'] = $params['Limitation']; 02239 } 02240 } 02241 } 02242 } 02243 02244 return $accessAllowed; 02245 } 02246 02247 /*! 02248 \return an array of roles which the user is assigned to 02249 */ 02250 function roles() 02251 { 02252 $groups = $this->attribute( 'groups' ); 02253 $groups[] = $this->attribute( 'contentobject_id' ); 02254 return eZRole::fetchByUser( $groups ); 02255 } 02256 02257 /*! 02258 \return an array of role ids which the user is assigned to 02259 */ 02260 function roleIDList() 02261 { 02262 if ( eZINI::instance()->variable( 'RoleSettings', 'EnableCaching' ) === 'true' ) 02263 { 02264 $userCache = $this->getUserCache(); 02265 return $userCache['roles']; 02266 } 02267 02268 $groups = $this->attribute( 'groups' ); 02269 $groups[] = $this->attribute( 'contentobject_id' ); 02270 return eZRole::fetchIDListByUser( $groups ); 02271 } 02272 02273 /*! 02274 \return an array of limited assignments 02275 */ 02276 function limitList( $useGroupsCache = true ) 02277 { 02278 if ( $useGroupsCache ) 02279 $groups = $this->groups(); 02280 else 02281 $groups = $this->generateGroupIdList(); 02282 $groups[] = $this->attribute( 'contentobject_id' ); 02283 $groups = implode( ', ', $groups ); 02284 02285 $db = eZDB::instance(); 02286 02287 $limitationsArray = $db->arrayQuery( "SELECT DISTINCT limit_identifier, limit_value 02288 FROM ezuser_role 02289 WHERE contentobject_id IN ( $groups )" ); 02290 02291 return $limitationsArray; 02292 } 02293 02294 /*! 02295 \return an array of values of limited assignments 02296 */ 02297 function limitValueList() 02298 { 02299 if ( eZINI::instance()->variable( 'RoleSettings', 'EnableCaching' ) === 'true' ) 02300 { 02301 $userCache = $this->getUserCache(); 02302 return $userCache['role_limitations']; 02303 } 02304 02305 $limitValueList = array(); 02306 $limitList = $this->limitList(); 02307 foreach ( $limitList as $limit ) 02308 { 02309 $limitValueList[] = $limit['limit_value']; 02310 } 02311 02312 return $limitValueList; 02313 } 02314 02315 function contentObject() 02316 { 02317 if ( isset( $this->ContentObjectID ) and $this->ContentObjectID ) 02318 { 02319 return eZContentObject::fetch( $this->ContentObjectID ); 02320 } 02321 return null; 02322 } 02323 02324 /** 02325 * Returns the eZUserAccountKey associated with this user 02326 * 02327 * @return eZUserAccountKey 02328 */ 02329 public function accountKey() 02330 { 02331 return eZUserAccountKey::fetchByUserID( $this->ContentObjectID ); 02332 } 02333 02334 /*! 02335 Returns true if it's a real user which is logged in. False if the user 02336 is the default user or the fallback buildtin user. 02337 */ 02338 function isLoggedIn() 02339 { 02340 if ( $this->ContentObjectID == self::anonymousId() or 02341 $this->ContentObjectID == -1 ) 02342 { 02343 return false; 02344 } 02345 return true; 02346 } 02347 02348 /*! 02349 \return an array of id's with all the groups the user belongs to. 02350 */ 02351 function groups( $asObject = false ) 02352 { 02353 if ( $asObject == true ) 02354 { 02355 if ( !isset( $this->GroupsAsObjects ) ) 02356 { 02357 $db = eZDB::instance(); 02358 $contentobjectID = $this->attribute( 'contentobject_id' ); 02359 $userGroups = $db->arrayQuery( "SELECT d.*, c.path_string 02360 FROM ezcontentobject_tree b, 02361 ezcontentobject_tree c, 02362 ezcontentobject d 02363 WHERE b.contentobject_id='$contentobjectID' AND 02364 b.parent_node_id = c.node_id AND 02365 d.id = c.contentobject_id 02366 ORDER BY c.contentobject_id "); 02367 $userGroupArray = array(); 02368 $pathArray = array(); 02369 foreach ( $userGroups as $group ) 02370 { 02371 $pathItems = explode( '/', $group["path_string"] ); 02372 array_pop( $pathItems ); 02373 array_pop( $pathItems ); 02374 foreach ( $pathItems as $pathItem ) 02375 { 02376 if ( $pathItem != '' && $pathItem > 1 ) 02377 $pathArray[] = $pathItem; 02378 } 02379 $userGroupArray[] = new eZContentObject( $group ); 02380 } 02381 $pathArray = array_unique( $pathArray ); 02382 02383 if ( !empty( $pathArray ) ) 02384 { 02385 $extraGroups = $db->arrayQuery( "SELECT d.* 02386 FROM ezcontentobject_tree c, 02387 ezcontentobject d 02388 WHERE c.node_id in ( " . implode( ', ', $pathArray ) . " ) AND 02389 d.id = c.contentobject_id 02390 ORDER BY c.contentobject_id "); 02391 foreach ( $extraGroups as $group ) 02392 { 02393 $userGroupArray[] = new eZContentObject( $group ); 02394 } 02395 } 02396 02397 $this->GroupsAsObjects = $userGroupArray; 02398 } 02399 return $this->GroupsAsObjects; 02400 } 02401 else 02402 { 02403 if ( !isset( $this->Groups ) ) 02404 { 02405 if ( eZINI::instance()->variable( 'RoleSettings', 'EnableCaching' ) === 'true' ) 02406 { 02407 $userCache = $this->getUserCache(); 02408 $this->Groups = $userCache['groups']; 02409 } 02410 else 02411 { 02412 $this->Groups = $this->generateGroupIdList(); 02413 } 02414 } 02415 return $this->Groups; 02416 } 02417 } 02418 02419 /** 02420 * Generate list of group id's 02421 * 02422 * @since 4.4 02423 * @return array 02424 */ 02425 protected function generateGroupIdList() 02426 { 02427 $db = eZDB::instance(); 02428 $userID = $this->ContentObjectID; 02429 $userGroups = $db->arrayQuery( "SELECT c.contentobject_id as id,c.path_string 02430 FROM ezcontentobject_tree b, 02431 ezcontentobject_tree c 02432 WHERE b.contentobject_id='$userID' AND 02433 b.parent_node_id = c.node_id 02434 ORDER BY c.contentobject_id "); 02435 $pathArray = array(); 02436 $userGroupArray = array(); 02437 foreach ( $userGroups as $group ) 02438 { 02439 $pathItems = explode( '/', $group["path_string"] ); 02440 array_pop( $pathItems ); 02441 array_pop( $pathItems ); 02442 foreach ( $pathItems as $pathItem ) 02443 { 02444 if ( $pathItem != '' && $pathItem > 1 ) 02445 $pathArray[] = $pathItem; 02446 } 02447 $userGroupArray[] = $group['id']; 02448 } 02449 02450 if ( !empty( $pathArray ) ) 02451 { 02452 $pathArray = array_unique ($pathArray); 02453 $extraGroups = $db->arrayQuery( "SELECT c.contentobject_id as id 02454 FROM ezcontentobject_tree c, 02455 ezcontentobject d 02456 WHERE c.node_id in ( " . implode( ', ', $pathArray ) . " ) AND 02457 d.id = c.contentobject_id 02458 ORDER BY c.contentobject_id "); 02459 foreach ( $extraGroups as $group ) 02460 { 02461 $userGroupArray[] = $group['id']; 02462 } 02463 } 02464 return $userGroupArray; 02465 } 02466 02467 /*! 02468 Checks if user is logged in, if not and the site requires user login for access 02469 a module redirect is returned. 02470 02471 \return null, user login not required. 02472 */ 02473 function checkUser( &$siteBasics, $uri ) 02474 { 02475 $http = eZHTTPTool::instance(); 02476 $check = array( "module" => "user", 02477 "function" => "login" ); 02478 if ( eZSession::issetkey( 'eZUserLoggedInID', false ) && 02479 $http->sessionVariable( "eZUserLoggedInID" ) != '' && 02480 $http->sessionVariable( "eZUserLoggedInID" ) != self::anonymousId() ) 02481 { 02482 $currentUser = eZUser::currentUser(); 02483 if ( !$currentUser->isEnabled() ) 02484 { 02485 eZUser::logoutCurrent(); 02486 $currentUser = eZUser::currentUser(); 02487 } 02488 else 02489 { 02490 return null; 02491 } 02492 } 02493 02494 $ini = eZINI::instance(); 02495 $moduleName = $uri->element(); 02496 $viewName = $uri->element( 1 ); 02497 $anonymousAccessList = $ini->variable( "SiteAccessSettings", "AnonymousAccessList" ); 02498 foreach ( $anonymousAccessList as $anonymousAccess ) 02499 { 02500 $elements = explode( '/', $anonymousAccess ); 02501 if ( !isset( $elements[1] ) ) 02502 { 02503 if ( $moduleName == $elements[0] ) 02504 { 02505 return null; 02506 } 02507 } 02508 else 02509 { 02510 if ( $moduleName == $elements[0] and 02511 $viewName == $elements[1] ) 02512 { 02513 return null; 02514 } 02515 } 02516 } 02517 02518 return $check; 02519 } 02520 02521 /*! 02522 Funtion performed before user login info is collected. 02523 It's optional to implement this function in new login handler. 02524 02525 \return @see eZUserLoginHandler::checkUser() 02526 */ 02527 function preCollectUserInfo() 02528 { 02529 return array( 'module' => 'user', 'function' => 'login' ); 02530 } 02531 02532 /*! 02533 Function performed after user login info has been collected. 02534 Store login data as array: 02535 array( 'login' => <username>, 02536 'password' = <password> ) 02537 to session variable EZ_LOGIN_HANDLER_USER_INFO for automatic processing of login data. 02538 02539 \return @see eZUserLoginHandler::checkUser() 02540 */ 02541 function postCollectUserInfo() 02542 { 02543 return true; 02544 } 02545 02546 /*! 02547 Check if login handler require special login URI 02548 02549 \return Special login uri. If false, use system standard login uri. 02550 */ 02551 function loginURI() 02552 { 02553 return false; 02554 } 02555 02556 /*! 02557 Check if login handler require forced login at user check. 02558 02559 \return true if force login on user check, false if not. 02560 */ 02561 function forceLogin() 02562 { 02563 return false; 02564 } 02565 02566 /** 02567 * Creates the cache path if it doesn't exist, and returns the cache 02568 * directory. The $userId parameter is used to create multi-level directory names 02569 * 02570 * @params int $userId 02571 * @return string Cache directory for the user 02572 */ 02573 static function getCacheDir( $userId = 0 ) 02574 { 02575 $dir = eZSys::cacheDirectory() . '/user-info' . eZDir::createMultilevelPath( $userId, 5 ); 02576 02577 if ( !is_dir( $dir ) ) 02578 { 02579 eZDir::mkdir( $dir, false, true ); 02580 } 02581 return $dir; 02582 } 02583 02584 /** 02585 * Expire user access / info cache globally 02586 */ 02587 static function cleanupCache() 02588 { 02589 eZExpiryHandler::registerShutdownFunction(); 02590 $handler = eZExpiryHandler::instance(); 02591 $handler->setTimestamp( 'user-info-cache', time() ); 02592 $handler->store(); 02593 } 02594 02595 /** 02596 * Returns the filename for a cache file with user information 02597 * 02598 * @deprecated In 4.4.0 02599 * @params int $userId 02600 * @return string|false Filename of the cachefile, or false when the user should not be cached 02601 */ 02602 static function getCacheFilename( $userId ) 02603 { 02604 $ini = eZINI::instance(); 02605 $cacheUserPolicies = $ini->variable( 'RoleSettings', 'UserPolicyCache' ); 02606 if ( $cacheUserPolicies === 'enabled' ) 02607 { 02608 return eZUser::getCacheDir( $userId ). '/user-'. $userId . '.cache.php'; 02609 } 02610 else if ( $cacheUserPolicies !== 'disabled' ) 02611 { 02612 $cachableIDs = explode( ',', $cacheUserPolicies ); 02613 if ( in_array( $userId, $cachableIDs ) ) 02614 { 02615 return eZUser::getCacheDir( $userId ). '/user-'. $userId . '.cache.php'; 02616 } 02617 } 02618 return false; 02619 } 02620 02621 static function fetchUserClassList( $asObject = false, $fields = false ) 02622 { 02623 // Get names of user classes 02624 if ( !$asObject and 02625 is_array( $fields ) and 02626 !empty( $fields ) ) 02627 { 02628 $fieldsFilter = ''; 02629 $i = 0; 02630 foreach ( $fields as $fieldName ) 02631 { 02632 if ( $i > 0 ) 02633 $fieldsFilter .= ', '; 02634 $fieldsFilter .= 'ezcontentclass.' . $fieldName; 02635 $i++; 02636 } 02637 } 02638 else 02639 { 02640 $fieldsFilter = 'ezcontentclass.*'; 02641 } 02642 $db = eZDB::instance(); 02643 $userClasses = $db->arrayQuery( "SELECT $fieldsFilter 02644 FROM ezcontentclass, ezcontentclass_attribute 02645 WHERE ezcontentclass.id = ezcontentclass_attribute.contentclass_id AND 02646 ezcontentclass.version = " . eZContentClass::VERSION_STATUS_DEFINED ." AND 02647 ezcontentclass_attribute.version = 0 AND 02648 ezcontentclass_attribute.data_type_string = 'ezuser'" ); 02649 02650 return eZPersistentObject::handleRows( $userClasses, "eZContentClass", $asObject ); 02651 } 02652 02653 static function fetchUserClassNames() 02654 { 02655 $userClassNames = array(); 02656 $userClasses = eZUser::fetchUserClassList( false, array( 'identifier' ) ); 02657 foreach ( $userClasses as $class ) 02658 { 02659 $userClassNames[] = $class[ 'identifier' ]; 02660 } 02661 return $userClassNames; 02662 } 02663 02664 static function fetchUserGroupClassNames() 02665 { 02666 // Get names of user classes 02667 $userClassNames = array(); 02668 $userClasses = eZUser::fetchUserClassList( false, array( 'identifier' ) ); 02669 foreach ( $userClasses as $class ) 02670 { 02671 $userClassNames[] = $class[ 'identifier' ]; 02672 } 02673 02674 // Get names of all allowed content-classes for the Users subtree 02675 $contentIni = eZINI::instance( "content.ini" ); 02676 $userGroupClassNames = array(); 02677 if ( $contentIni->hasVariable( 'ClassGroupIDs', 'Users' ) and 02678 is_numeric( $usersClassGroupID = $contentIni->variable( 'ClassGroupIDs', 'Users' ) ) and 02679 count( $usersClassList = eZContentClassClassGroup::fetchClassList( eZContentClass::VERSION_STATUS_DEFINED, $usersClassGroupID ) ) > 0 ) 02680 { 02681 foreach ( $usersClassList as $userClass ) 02682 { 02683 $userGroupClassNames[] = $userClass->attribute( 'identifier' ); 02684 } 02685 } 02686 02687 // Get names of user-group classes 02688 $groupClassNames = array_diff( $userGroupClassNames, $userClassNames ); 02689 return $groupClassNames; 02690 } 02691 02692 /*! 02693 Checks the password for validity 02694 \static 02695 \return true when password is valid by length and not empty, false if not 02696 */ 02697 static function validatePassword( $password ) 02698 { 02699 $ini = eZINI::instance(); 02700 $minPasswordLength = $ini->variable( 'UserSettings', 'MinPasswordLength' ); 02701 if ( $password !== false and 02702 $password !== null and 02703 strlen( $password ) >= (int) $minPasswordLength ) 02704 { 02705 return true; 02706 } 02707 02708 return false; 02709 } 02710 02711 /** 02712 * Validates user login name using site.ini[UserSettings]UserNameValidationRegex[] 02713 * 02714 * @static 02715 * @since Version 4.1 02716 * @param string $loginName that we want to validate. 02717 * @param string $errorText by reference for details if validation fails. 02718 * @return bool Indicates if validation failed (false) or not (true). 02719 */ 02720 static function validateLoginName( $loginName, &$errorText ) 02721 { 02722 $ini = eZINI::instance(); 02723 $regexList = $ini->variable( 'UserSettings', 'UserNameValidationRegex' ); 02724 $errorTextList = $ini->variable( 'UserSettings', 'UserNameValidationErrorText' ); 02725 foreach ( $regexList as $key => $regex ) 02726 { 02727 if( preg_match( $regex, $loginName) ) 02728 { 02729 if ( isset( $errorTextList[$key] ) ) 02730 $errorText = $errorTextList[$key]; 02731 else 02732 $errorText = $ini->variable( 'UserSettings', 'DefaultUserNameValidationErrorText' ); 02733 return false; 02734 } 02735 } 02736 return true; 02737 } 02738 02739 /** 02740 * Gets the id of the anonymous user. 02741 * 02742 * @static 02743 * @return int User id of anonymous user. 02744 */ 02745 public static function anonymousId() 02746 { 02747 if ( self::$anonymousId === null ) 02748 { 02749 $ini = eZINI::instance(); 02750 self::$anonymousId = (int)$ini->variable( 'UserSettings', 'AnonymousUserID' ); 02751 $GLOBALS['eZUserBuiltins'] = array( self::$anonymousId ); 02752 } 02753 return self::$anonymousId; 02754 } 02755 02756 /*! 02757 Returns the IDs of content classes that contain user accounts 02758 */ 02759 public static function contentClassIDs() 02760 { 02761 $userContentClassIDs = array(); 02762 02763 $ini = eZINI::instance( 'content.ini' ); 02764 $userDatatypes = $ini->variable( "DataTypeSettings", "UserDataTypes" ); 02765 02766 $userContentClassIDs = array(); 02767 foreach ( $userDatatypes as $datatypeIdentifier ) 02768 { 02769 $userContentClassIDs = array_merge( $userContentClassIDs, eZContentClass::fetchIDListContainingDatatype( $datatypeIdentifier ) ); 02770 } 02771 02772 return $userContentClassIDs; 02773 } 02774 02775 public function canLoginToSiteAccess( $access ) 02776 { 02777 $siteAccessResult = $this->hasAccessTo( 'user', 'login' ); 02778 $hasAccessToSite = false; 02779 02780 if ( $siteAccessResult[ 'accessWord' ] == 'limited' ) 02781 { 02782 $siteNameCRC = eZSys::ezcrc32( $access[ 'name' ] ); 02783 $policyChecked = false; 02784 foreach ( $siteAccessResult['policies'] as $policy ) 02785 { 02786 if ( isset( $policy['SiteAccess'] ) ) 02787 { 02788 $policyChecked = true; 02789 if ( in_array( $siteNameCRC, $policy['SiteAccess'] ) ) 02790 { 02791 $hasAccessToSite = true; 02792 break; 02793 } 02794 } 02795 } 02796 02797 if ( !$policyChecked ) 02798 { 02799 $hasAccessToSite = true; 02800 } 02801 } 02802 else if ( $siteAccessResult[ 'accessWord' ] == 'yes' ) 02803 { 02804 $hasAccessToSite = true; 02805 } 02806 02807 return $hasAccessToSite; 02808 } 02809 02810 /// \privatesection 02811 public $Login; 02812 public $Email; 02813 public $PasswordHash; 02814 public $PasswordHashType; 02815 public $Groups; 02816 public $OriginalPassword; 02817 public $OriginalPasswordConfirm; 02818 02819 /** 02820 * Holds user cache like user info and access array 02821 * 02822 * @since 4.4 02823 * @see eZUser::getUserCache() 02824 * @var array|null 02825 */ 02826 protected $UserCache = null; 02827 02828 /** 02829 * Used to keep track that a logout was performed, and therefore prevent 02830 * auto-login from happening if an SSO is used 02831 * @var bool 02832 * @since 4.3 02833 */ 02834 protected static $userHasLoggedOut = false; 02835 } 02836 02837 ?>