- <?php
- /**
- * This is the user model. Each user corresponds to a
- * user in Perforce.
- *
- * @copyright 2011 Perforce Software. All rights reserved.
- * @license Please see LICENSE.txt in top-level folder of this distribution.
- * @version <release>/<patch>
- */
- class P4Cms_User extends P4Cms_Record_Connected implements Zend_Auth_Adapter_Interface
- {
- const FETCH_BY_NAME = 'name';
- const FETCH_MAXIMUM = 'maximum';
- const FETCH_SYSTEM_USER = 'systemUser';
-
- protected $_p4User = null;
- protected $_personalAdapter = null;
- protected static $_rolesCache = array();
-
- protected static $_activeUser = null;
- protected static $_acl = null;
- protected static $_idField = 'id';
- protected static $_fields = array(
- 'fullName' => array(
- 'accessor' => 'getFullName',
- 'mutator' => 'setFullName'
- ),
- 'email' => array(
- 'accessor' => 'getEmail',
- 'mutator' => 'setEmail'
- ),
- 'password' => array(
- 'accessor' => 'getPassword',
- 'mutator' => 'setPassword'
- )
- );
-
- /**
- * Clear the static roles cache entirely.
- */
- public static function clearRolesCache()
- {
- static::$_rolesCache = array();
- }
-
- /**
- * Check if the named user exists.
- *
- * @param string $username the username of the user to look for.
- * @param array|null $options optional - no options are presently supported.
- * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use.
- * @return bool true if the user exists, false otherwise.
- */
- public static function exists($username, $options = null, P4Cms_Record_Adapter $adapter = null)
- {
- if (!is_array($options) && !is_null($options)) {
- throw new InvalidArgumentException(
- 'Options must be an array or null'
- );
- }
-
- try {
- static::fetch($username, null, $adapter);
- return true;
- } catch (P4Cms_Model_NotFoundException $e) {
- return false;
- } catch (InvalidArgumentException $e) {
- return false;
- }
- }
-
- /**
- * Fetch the named user.
- *
- * @param string $username the username of the user to fetch.
- * @param array|null $options optional - no options are presently supported.
- * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use.
- * @return P4Cms_User instance of the requested user.
- * @throws P4Cms_Model_NotFoundException if the requested user does not exist.
- */
- public static function fetch($username, array $options = null, P4Cms_Record_Adapter $adapter = null)
- {
- if (!is_array($options) && !is_null($options)) {
- throw new InvalidArgumentException(
- 'Options must be an array or null'
- );
- }
-
- $adapter = $adapter ?: static::getDefaultAdapter();
-
- // attempt to fetch user from perforce.
- try {
- $p4User = P4_User::fetch($username, $adapter->getConnection());
- } catch (P4_Spec_NotFoundException $e) {
- throw new P4Cms_Model_NotFoundException(
- "Cannot fetch user. User '$username' does not exist."
- );
- }
-
- // create new user instance
- $user = new static;
- $user->setAdapter($adapter)
- ->setId($username)
- ->_setP4User($p4User);
-
- return $user;
- }
-
- /**
- * Fetch all users in the system (ie. get users from Perforce).
- *
- * @param array $options optional - array of options to augment fetch behavior.
- * supported options are:
- *
- * FETCH_MAXIMUM - set to integer value to limit to the
- * first 'max' number of entries.
- * FETCH_BY_NAME - set to user name pattern (e.g. 'jdo*'),
- * can be a single string or array of strings.
- * FETCH_SYSTEM_USER - set to true to include the system user
- * defaults to false (system user is excluded)
- *
- * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use.
- * @return P4Cms_Model_Iterator all users in the system.
- */
- public static function fetchAll(
- array $options = null,
- P4Cms_Record_Adapter $adapter = null)
- {
- $adapter = $adapter ?: static::getDefaultAdapter();
-
- $users = new P4Cms_Model_Iterator;
- foreach (P4_User::fetchAll($options, $adapter->getConnection()) as $p4User) {
- $user = new static;
- $user->setAdapter($adapter)
- ->setId($p4User->getId())
- ->_setP4User($p4User);
-
- $users[] = $user;
- }
-
- // exclude system user by default
- if ((!isset($options[static::FETCH_SYSTEM_USER]) || !$options[static::FETCH_SYSTEM_USER])
- && P4Cms_Site::hasActive()
- ) {
- // we assume the active site is running as the system user; get the id
- $systemUser = P4Cms_Site::fetchActive()->getConnection()->getUser();
- $users->filter('id', $systemUser, array(P4Cms_Model_Iterator::FILTER_INVERSE));
- }
-
- return $users;
- }
-
- /**
- * Fetch all role member users.
- *
- * @param P4Cms_Acl_Role|string|array $role role or list of roles to fetch members of.
- * @param P4Cms_Record_Adapter $adapter optional, storage adapter to use.
- * @return P4Cms_Model_Iterator role(s) member users.
- */
- public static function fetchByRole($role, P4Cms_Record_Adapter $adapter = null)
- {
- if (is_string($role) || $role instanceof P4Cms_Acl_Role) {
- $roles = array($role);
- } else if (is_array($role)) {
- $roles = $role;
- } else {
- throw new InvalidArgumentException(
- "Role must be an instance of P4Cms_Acl_Role or a string or an array."
- );
- }
-
- $users = array();
- foreach ($roles as $role) {
- // if role is not instance of P4Cms_Acl_Role, try to fetch it
- if (!$role instanceof P4Cms_Acl_Role) {
- if (!P4Cms_Acl_Role::exists($role, null, $adapter)) {
- break;
- }
- $role = P4Cms_Acl_Role::fetch($role, null, $adapter);
- }
-
- // add role users to the users list
- $users = array_merge($users, $role->getUsers());
- }
-
- // early exit if no users to fetch
- if (!count($users)) {
- return new P4Cms_Model_Iterator;
- }
-
- // fetch all member users
- return static::fetchAll(array(static::FETCH_BY_NAME => array_unique($users)), $adapter);
- }
-
- /**
- * Count all users - extended to route through fetch all.
- *
- * @param array $options optional - array of options to augment count
- * @param P4Cms_Record_Adapter $adapter optional - storage adapter to use.
- * @return integer The count of all matching records
- */
- public static function count(
- $options = array(),
- P4Cms_Record_Adapter $adapter = null)
- {
- return static::fetchAll($options, $adapter)->count();
- }
-
- /**
- * Return the user's email-address.
- *
- * @return string|null the user's email address
- */
- public function getEmail()
- {
- return $this->_getP4User()->getEmail();
- }
-
- /**
- * Return the user's full name.
- *
- * @return string|null the user's full name
- */
- public function getFullName()
- {
- return $this->_getP4User()->getFullName();
- }
-
- /**
- * Get the in-memory password (if one is set).
- *
- * @return string|null the in-memory password.
- */
- public function getPassword()
- {
- return $this->_getP4User()->getPassword();
- }
-
- /**
- * Fetch the currently active user.
- * Guaranteed to return the active user model or throw an exception.
- *
- * @return P4Cms_User the currently active user.
- * @throws P4Cms_User_Exception if there is no currently active user.
- * @todo throw a specific type of exception.
- */
- public static function fetchActive()
- {
- if (!static::$_activeUser || !static::$_activeUser instanceof P4Cms_User) {
- throw new P4Cms_User_Exception("There is no currently active user.");
- }
-
- return static::$_activeUser;
- }
-
- /**
- * Determine if there is an active user.
- *
- * @return boolean true if there is an active user
- */
- public static function hasActive()
- {
- try {
- static::fetchActive();
- return true;
- } catch (Exception $e) {
- return false;
- }
- }
-
- /**
- * Set the active user.
- *
- * @param P4Cms_User $user the user model instance to make active.
- */
- public static function setActive(P4Cms_User $user)
- {
- static::$_activeUser = $user;
- }
-
- /**
- * Clear the active user.
- */
- public static function clearActive()
- {
- static::$_activeUser = null;
- }
-
- /**
- * Determine if this user is anonymous (has no id).
- *
- * @return bool true if the user is anonymous.
- */
- public function isAnonymous()
- {
- return !(bool) strlen($this->getId());
- }
-
- /**
- * Determine if this user has member role.
- *
- * @return bool true if the user has member role, false otherwise.
- */
- public function isMember()
- {
- return in_array(P4Cms_Acl_Role::ROLE_MEMBER, $this->getRoles()->invoke('getId'));
- }
-
- /**
- * Determine if this user has administrator role.
- *
- * @return bool true if the user has administrator role, false otherwise.
- */
- public function isAdministrator()
- {
- return in_array(P4Cms_Acl_Role::ROLE_ADMINISTRATOR, $this->getRoles()->invoke('getId'));
- }
-
- /**
- * Test if the given password is correct for this user.
- *
- * @param string $password the password to test.
- * @return bool true if the password is correct, false otherwise.
- */
- public function isPassword($password)
- {
- return $this->_getP4User()->isPassword($password);
- }
-
- /**
- * Determine if this user is allowed to access a particular resource
- * and (optionally) a particular privilege on the resource.
- *
- * @param P4Cms_Acl_Resource|string $resource the resource to check access to.
- * @param P4Cms_Acl_Privilege|string|null $privilege optional - the privilege to check.
- * @param P4Cms_Acl|null $acl optional - the acl to check against.
- * defaults to the currently active acl.
- * @return bool true if the user is allowed access to the resource.
- *
- * @publishes p4cms.acl.users.privileges
- * Gathers the resource privileges for authorization checks, or for presentation by
- * the User module.
- * P4Cms_Acl_Resource $resource The resource that must be checked for
- * appropriate privileges.
- */
- public function isAllowed($resource, $privilege = null, P4Cms_Acl $acl = null)
- {
- $acl = $acl ?: P4Cms_Acl::fetchActive();
-
- // user is allowed access if any of the roles are.
- foreach ($this->getRoles() as $role) {
- try {
- if ($acl->isAllowed($role, $resource, $privilege)) {
- return true;
- }
- } catch (Zend_Acl_Exception $e) {
- // acl throws if the resource doesn't exist, but
- // we don't consider this a throw-able offense here.
- // we do however treat it as permission denied.
- }
- }
-
- return false;
- }
-
- /**
- * Return list of all privileges for which user has access to a given resource.
- *
- * @param P4Cms_Acl_Resource|string $resource the resource to check access to.
- * @param P4Cms_Acl|null $acl optional - the acl to check against.
- * defaults to the currently active acl.
- * @return array list of all privileges for which user
- * user has access to a given resource.
- */
- public function getAllowedPrivileges($resource, P4Cms_Acl $acl = null)
- {
- $acl = $acl ?: P4Cms_Acl::fetchActive();
- $roles = $this->getRoles()->toArray(true);
- $privileges = array();
-
- // user is allowed access if any of the roles are.
- foreach ($roles as $role) {
- $privileges = array_merge(
- $privileges,
- $acl->getAllowedPrivileges($role, $resource)
- );
- }
-
- return array_unique($privileges);
- }
-
- /**
- * Get the roles that this user belongs to.
- * Caches the results of P4Cms_Acl_Role::fetchAll().
- *
- * @return P4Cms_Model_Iterator the roles that this user is a member of.
- */
- public function getRoles()
- {
- // if user is un-identified, user belongs to anonymous role
- if ($this->isAnonymous()) {
- $role = new P4Cms_Acl_Role;
- $role->setId(P4Cms_Acl_Role::ROLE_ANONYMOUS);
- $roles = new P4Cms_Model_Iterator;
- $roles[] = $role;
-
- return $roles;
- }
-
- // for other users, roles are cached based on the adapter and user id
- $adapter = $this->getAdapter();
- $userId = $this->getId();
- $cacheKey = spl_object_hash($adapter) . md5($userId);
-
- // load the user roles (but only fetch them once)
- if (!array_key_exists($cacheKey, static::$_rolesCache)) {
- // fetch roles that user is a member of
- $roles = P4Cms_Acl_Role::fetchAll(
- array(P4Cms_Acl_Role::FETCH_BY_MEMBER => $userId),
- $adapter
- );
-
- static::$_rolesCache[$cacheKey] = $roles;
- }
-
- return static::$_rolesCache[$cacheKey];
- }
-
- /**
- * Generate a single role that inherits from all of the roles
- * that this user has and register it with the acl temporarily
- * (the role is not saved).
- *
- * This allows us to specify a single role when checking if the
- * user is allowed access to a given resource/privilege.
- *
- * @param P4Cms_Acl|null $acl optional - the acl to check against.
- * defaults to the currently active acl.
- * @return string the id of the generated role combining
- * all this user's roles or the id of an
- * existing role if the user has only one.
- * @throws P4Cms_User_Exception if the user has no roles.
- */
- public function getAggregateRole(P4Cms_Acl $acl = null)
- {
- $acl = $acl ?: P4Cms_Acl::fetchActive();
-
- // can't get aggregate role if no roles.
- $roles = $this->getRoles();
- if (count($roles) == 0) {
- throw new P4Cms_User_Exception(
- "Cannot get aggregate role for a user with no roles."
- );
- }
-
- // no need to aggregate if user has one role.
- if (count($roles) <= 1) {
- return $roles->first()->getId();
- }
-
- // generate unique name.
- $i = 0;
- $roles = $roles->invoke('getId');
- $roleId = $this->getId() . "-" . implode('-', $roles);
- while ($acl->hasRole($roleId)) {
- $roleId = $this->getId() . "-" . implode('-', $roles) . "-" . ++$i;
- }
-
- // register role as super if any of the partial roles is super
- foreach ($roles as $role) {
- if (P4Cms_Acl_Role::isSuper($role)) {
- P4Cms_Acl_Role::addSuperRole($roleId);
- break;
- }
- }
-
- // add role to acl, but don't save role.
- $acl->addRole($roleId, $roles);
-
- return $roleId;
- }
-
- /**
- * Overrides parent to set adapter's connection for associated P4_User in addition.
- *
- * @param P4Cms_Record_Adapter $adapter the adapter to use for this instance.
- * @return P4Cms_User provides fluent interface.
- */
- public function setAdapter(P4Cms_Record_Adapter $adapter)
- {
- $this->_getP4User()->setConnection($adapter->getConnection());
- return parent::setAdapter($adapter);
- }
-
- /**
- * Set the user id - extended to proxy to p4 user.
- *
- * @param string|int|null $id the identifier of this record.
- * @return P4Cms_Record provides fluent interface.
- * @todo move more validation into record id validator
- * @todo reject empty strings ''.
- */
- public function setId($id)
- {
- $this->_getP4User()->setId($id);
-
- return parent::setId($id);
- }
-
- /**
- * Set the user's email-address.
- *
- * @param string|null $email the user's email address
- * @return P4Cms_User provides fluent interface
- */
- public function setEmail($email)
- {
- $this->_getP4User()->setEmail($email);
-
- return $this;
- }
-
- /**
- * Set the user's full name.
- *
- * @param string|null $name the user's full name
- * @return P4Cms_User provides fluent interface
- */
- public function setFullName($name)
- {
- $this->_getP4User()->setFullName($name);
-
- return $this;
- }
-
- /**
- * Set the user's password to the given password.
- * Does not take effect until save() is called.
- *
- * @param string|null $newPassword the new password string or
- * null to clear in-memory password.
- * @param string $oldPassword optional - existing password.
- * @return P4_User provides fluent interface.
- */
- public function setPassword($newPassword, $oldPassword = null)
- {
- $this->_getP4User()->setPassword($newPassword, $oldPassword);
-
- return $this;
- }
-
- /**
- * Generate a pseudo-random password, alternating consonants and vowels to
- * assist human readability. Password strength is flexible:
- *
- * 0 = lowercase letters only
- * 1 = add uppercase consonants
- * 2 = add uppercase vowels
- * 3 = add numbers
- * 4 = add special characters
- *
- * @param integer $length the desired length of the password.
- * @param integer $strength the desired strength of the password.
- * @return string the generated password.
- */
- public static function generatePassword($length, $strength = 0)
- {
- // vowels and consonants excluding the letters o, i and l
- // because they can be mistaken for other letters or numbers.
- $vowels = 'aeuy';
- $consonants = 'bcdfghjkmnpqrstvwxyz';
-
- if ($strength >= 1) {
- $consonants .= strtoupper($consonants);
- }
-
- if ($strength >= 2) {
- $vowels .= strtoupper($vowels);
- }
-
- // excludes the numbers 0 and 1 because they can be mistaken for letters.
- if ($strength >= 3) {
- $consonants .= '23456789';
- }
-
- if ($strength >= 4) {
- $consonants .= '@$%^';
- }
-
- $password = '';
- $alt = rand() % 2;
-
- for ($i = 0; $i < $length; $i++) {
- if ($alt == 1) {
- $password .= $consonants[ (rand() % strlen($consonants)) ];
- $alt = 0;
- } else {
- $password .= $vowels[ (rand() % strlen($vowels)) ];
- $alt = 1;
- }
- }
-
- return $password;
- }
-
- /**
- * Save this user entry.
- *
- * @return P4Cms_User provides fluent interface.
- */
- public function save()
- {
- // save the user spec.
- $this->_getP4User()->save();
-
- return $this;
- }
-
- /**
- * Delete this user entry.
- *
- * @return P4Cms_User provides fluent interface.
- */
- public function delete()
- {
- // if user with personal adapter (active user) is going to be deleted,
- // run disconnect callbacks before removing the user from Perforce,
- // otherwise user may be resurrected if disconnect callbacks use
- // user's connection (e.g. for user's workspace clean-up etc.)
- if ($this->hasPersonalAdapter()) {
- $connection = $this->getPersonalAdapter()->getConnection();
-
- // run disconnect callbacks and clear them after to ensure they
- // are not called again after user is removed from Perforce
- $connection->runDisconnectCallbacks()
- ->clearDisconnectCallbacks();
- }
-
- // delete the user spec last
- $this->_getP4User()->delete();
-
- // disconnect user with personal adapter
- if (isset($connection)) {
- $connection->disconnect();
- }
-
- return $this;
- }
-
- /**
- * Performs an authentication attempt
- *
- * @throws Zend_Auth_Adapter_Exception If authentication cannot be performed
- * @return Zend_Auth_Result
- */
- public function authenticate()
- {
- // authenticate against current p4 server.
- $p4 = P4_Connection::factory(
- $this->getAdapter()->getConnection()->getPort(),
- $this->getId(),
- null,
- $this->getPassword()
- );
-
- try {
- $ticket = $p4->login();
-
- // deny if user has no real roles
- if (!$this->getRoles()->count()) {
- return new Zend_Auth_Result(
- Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS,
- null,
- array('At least one role is required for successful authentication.')
- );
- }
-
- return new Zend_Auth_Result(
- Zend_Auth_Result::SUCCESS,
- array('id' => $this->getId(), 'ticket' => $ticket)
- );
- } catch (P4_Connection_LoginException $e) {
- return new Zend_Auth_Result(
- $e->getCode(),
- null,
- array($e->getMessage())
- );
- }
- }
-
- /**
- * Set the personal storage adapter for this user.
- *
- * @param P4Cms_Record_Adapter $adapter the personal adapter
- * @return P4Cms_User provides fluent interface
- */
- public function setPersonalAdapter(P4Cms_Record_Adapter $adapter = null)
- {
- $this->_personalAdapter = $adapter;
-
- return $this;
- }
-
- /**
- * Determine if a personalized adapter has been set for this user.
- *
- * @return bool true if a personal adapter is set; false otherwise.
- */
- public function hasPersonalAdapter()
- {
- try {
- $this->getPersonalAdapter();
- return true;
- } catch (P4Cms_User_Exception $e) {
- return false;
- }
- }
-
- /**
- * Get the personalized record storage adapter for this user.
- *
- * @return P4Cms_Record_Adapter a personalized storage adapter.
- * @throws P4Cms_User_Exception if no personal adapter has been set.
- */
- public function getPersonalAdapter()
- {
- // balk if no adapter set.
- if (!$this->_personalAdapter instanceof P4Cms_Record_Adapter) {
- throw new P4Cms_User_Exception(
- "Cannot get personal storage adapter. No personal adapter has been set."
- );
- }
-
- return $this->_personalAdapter;
- }
-
- /**
- * Generate a storage adapter that communicates with Perforce as this user.
- *
- * @param string $ticket optional - auth ticket to use for p4 connection
- * @param P4Cms_Site $site optional - site to get personal adapter for
- * (defaults to active site)
- * @return P4Cms_Record_Adapter a personalized storage adapter.
- */
- public function createPersonalAdapter($ticket = null, P4Cms_Site $site = null)
- {
- $site = $site ?: P4Cms_Site::fetchActive();
-
- // to avoid problems that result from multiple processes
- // sharing one client (namely race conditions), we generate
- // a temporary client for each request.
- $tempClientId = P4_Client::makeTempId();
-
- // create connection based on the active site.
- $connection = P4_Connection::factory(
- $site->getConnection()->getPort(),
- $this->getId(),
- $tempClientId,
- null,
- $ticket ?: null
- );
-
- // store client files under given site's workspaces path.
- $root = $site->getWorkspacesPath() . "/" . $tempClientId;
-
- // provide a custom clean-up callback to delete the workspace folder.
- $cleanup = function($entry, $defaultCallback) use ($root)
- {
- $defaultCallback($entry);
- P4Cms_FileUtility::deleteRecursive($root);
- };
-
- // create the client with the values we've setup above, using
- // makeTemp() so that it will be destroyed automatically.
- P4_Client::makeTemp(
- array(
- 'Client' => $tempClientId,
- 'Stream' => $site->getId(),
- 'Root' => $root
- ),
- $cleanup,
- $connection
- );
-
- // create personal adapter based on site adapter.
- $adapter = new P4Cms_Record_Adapter;
- $adapter->setConnection($connection)
- ->setBasePath("//" . $connection->getClient())
- ->setProperties($site->getStorageAdapter()->getProperties());
-
- return $adapter;
- }
-
- /**
- * Set the corresponding p4 user object instance.
- * Used when fetching users to prime the user object.
- *
- * @param P4_User $user the corresponding P4_User object.
- * @return P4Cms_User provides fluent interface.
- * @throws P4Cms_User_Exception if the user is anonymous or if the given user is not a
- * valid P4_User object.
- */
- protected function _setP4User($user)
- {
- // anonymous users can't have a corresponding perforce user.
- if ($this->isAnonymous()) {
- throw new P4Cms_User_Exception(
- "Cannot set p4 user for an anonymous user."
- );
- }
-
- if (!$user instanceof P4_User) {
- throw new P4Cms_User_Exception(
- "Cannot set p4 user. The given user is not a valid P4_User object."
- );
- }
-
- $this->_p4User = $user;
-
- return $this;
- }
-
- /**
- * Get the p4 user object that corresponds to this user.
- *
- * @return P4_User corresponding p4 user instance.
- */
- protected function _getP4User()
- {
- // only instantiate user once.
- if (!$this->_p4User instanceof P4_User) {
- $connection = $this->hasAdapter()
- ? $this->getAdapter()->getConnection()
- : null;
- $this->_p4User = new P4_User($connection);
- }
-
- return $this->_p4User;
- }
- }