User.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • library/
  • P4Cms/
  • User.php
  • View
  • Commits
  • Open Download .zip Download (27 KB)
<?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;
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/library/P4Cms/User.php
#1 8972 Matt Attaway Initial add of the Chronicle source code