- <?php
- /**
- * Abstracts operations against Perforce users.
- *
- * The P4 User class differs from the 'user' spec definition in that it
- * does not have a password field. This is because the password does
- * not behave like other fields. To change a user's password, use the
- * setPassword() function. To test if a given string matches a user's
- * password, use the isPassword() method. It is not possible to get a
- * user's password.
- *
- * @copyright 2011 Perforce Software. All rights reserved.
- * @license Please see LICENSE.txt in top-level folder of this distribution.
- * @version <release>/<patch>
- */
-
- namespace P4\Spec;
-
- use P4\Validate;
- use P4\Connection\Connection;
- use P4\Spec\Exception\Exception;
- use P4\Connection\ConnectionInterface;
- use P4\Connection\Exception\CommandException;
- use P4\Connection\Exception\LoginException;
- use P4\Model\Fielded\Iterator as FieldedIterator;
-
- class User extends PluralAbstract
- {
- const SPEC_TYPE = 'user';
- const ID_FIELD = 'User';
-
- const OPERATOR_USER = 'operator';
- const SERVICE_USER = 'service';
- const STANDARD_USER = 'standard';
-
- const FETCH_BY_NAME = 'name';
-
- protected $fields = array(
- 'Email' => array(
- 'accessor' => 'getEmail',
- 'mutator' => 'setEmail'
- ),
- 'Update' => array(
- 'accessor' => 'getUpdateDateTime'
- ),
- 'Access' => array(
- 'accessor' => 'getAccessDateTime'
- ),
- 'FullName' => array(
- 'accessor' => 'getFullName',
- 'mutator' => 'setFullName'
- ),
- 'JobView' => array(
- 'accessor' => 'getJobView',
- 'mutator' => 'setJobView'
- ),
- 'Reviews' => array(
- 'accessor' => 'getReviews',
- 'mutator' => 'setReviews'
- ),
- 'Password' => array(
- 'accessor' => 'getPassword',
- 'mutator' => 'setPassword'
- ),
- 'Type' => array(
- 'accessor' => 'getType',
- 'mutator' => 'setType'
- )
- );
-
- /**
- * Determine if the given user id exists.
- *
- * @param string $id the id to check for.
- * @param ConnectionInterface $connection optional - a specific connection to use.
- * @return bool true if the given id matches an existing user.
- */
- public static function exists($id, ConnectionInterface $connection = null)
- {
- // check id for valid format
- if (!static::isValidId($id)) {
- return false;
- }
-
- $users = static::fetchAll(
- array(
- static::FETCH_BY_NAME => $id,
- static::FETCH_MAXIMUM => 1
- ),
- $connection
- );
-
- return (bool) count($users);
- }
-
- /**
- * Get all users from Perforce. Adds filtering option.
- *
- * @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.
- *
- * @param ConnectionInterface $connection optional - a specific connection to use.
- * @return FieldedIterator all records of this type.
- */
- public static function fetchAll($options = array(), ConnectionInterface $connection = null)
- {
- $connection = $connection ?: static::getDefaultConnection();
-
- // if not fetching by name, defer to parent
- if (!isset($options[static::FETCH_BY_NAME])) {
- return parent::fetchAll($options, $connection);
- }
-
- // get fetch max option and uset it from the options as we handle it manually
- $max = isset($options[static::FETCH_MAXIMUM]) ? $options[static::FETCH_MAXIMUM] : 0;
- unset($options[static::FETCH_MAXIMUM]);
-
- // sort names before fetching users from the server, so if max is set
- // we get the first max users (according to case-sensitivity of server)
- $names = (array) $options[static::FETCH_BY_NAME];
- if ($connection->isCaseSensitive()) {
- sort($names);
- } else {
- usort($names, 'strcasecmp');
- }
-
- // fetch users in several (as few as possible) runs as
- // there is a potential to exceed the arg-max on this command
- $users = new FieldedIterator;
- foreach ($connection->batchArgs($names) as $batch) {
- $options[static::FETCH_BY_NAME] = $batch;
- foreach (parent::fetchAll($options, $connection) as $user) {
- $users[$user->getId()] = $user;
-
- // exit loop if we've reached the max limit
- if ($max && $users->count() == $max) {
- break(2);
- }
- }
- }
-
- return $users;
- }
-
- /**
- * Save this user to Perforce. This will not save changes to the
- * password field. Passwords must be set via setPassword().
- *
- * @return SpecAbstract provides a fluent interface
- * @throws Exception if no id has been set.
- */
- public function save()
- {
- // ensure all required fields have values.
- $this->validateRequiredFields();
-
- // set 'password' field to '******' otherwise password
- // will be deleted under certain security levels.
- $values = $this->getRawValues();
- $values['Password'] = '******';
-
- // initialize command flags with first arg.
- $flags = array("-i");
-
- // if we are connected with super user privileges, add in the -f flag.
- // otherwise, if not connected as this user, connect as this user.
- $connection = $this->getConnection();
- if ($connection->isSuperUser()) {
- $flags[] = "-f";
- } elseif ($connection->getUser() != $this->getId()) {
- $connection = Connection::factory(
- $connection->getPort(),
- $this->getId()
- );
- }
-
- // send user spec to server.
- $password = $this->getRawValue('Password');
- try {
- $connection->run(static::SPEC_TYPE, $flags, $values);
- } catch (CommandException $e) {
-
- // if saving user failed because password has not been
- // set, and the caller supplied a password, try setting
- // the password first, then saving user again.
- //
- // @todo This workaround relies on the fact, that user has been created although previous
- // command had failed. At the moment it seems, that when adding a new user (with non-superuser
- // connection) this error cannot be avoided.
- $errors = $e->getResult()->getErrors();
- if (stristr($errors[0], "password must be set") && is_array($password) && isset($password[0])) {
- $this->changePassword($password[0], null, $connection);
- $connection->run(static::SPEC_TYPE, $flags, $values);
- // avoid redundant password change
- $password[0] = null;
- } else {
- throw $e;
- }
- }
-
- // change the users password if they have set a new one.
- if (is_array($password) && $password[0] !== null) {
- $this->changePassword($password[0], $password[1], $connection);
- }
-
- // should re-populate (server may change values).
- $this->deferPopulate(true);
-
- return $this;
- }
-
- /**
- * Remove this user from Perforce.
- *
- * @return SpecAbstract provides a fluent interface
- * @throws Exception if no id has been set.
- */
- public function delete()
- {
- if ($this->getId() === null) {
- throw new Exception("Cannot delete. No id has been set.");
- }
-
- // initialize command flags with first arg.
- $flags = array("-d");
-
- // if we are connected with super user privileges, add in the -f flag.
- // otherwise, if not connected as this user, connect as this user.
- $connection = $this->getConnection();
- if ($connection->isSuperUser()) {
- $flags[] = "-f";
- } elseif ($connection->getUser() != $this->getId()) {
- $connection = Connection::factory(
- $connection->getPort(),
- $this->getId()
- );
- }
-
- // issue delete user command.
- $flags[] = $this->getId();
- $result = $connection->run(static::SPEC_TYPE, $flags);
-
- // should re-populate.
- $this->deferPopulate(true);
-
- return $this;
- }
-
- /**
- * Get the in-memory password (if one is set).
- *
- * @return string|null the in-memory password.
- */
- public function getPassword()
- {
- $password = $this->getRawValue('Password');
- return is_array($password) ? $password[0] : null;
- }
-
- /**
- * 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 User provides fluent interface.
- */
- public function setPassword($newPassword, $oldPassword = null)
- {
- $this->setRawValue('Password', array($newPassword, $oldPassword));
-
- return $this;
- }
-
- /**
- * 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)
- {
- $p4 = Connection::factory(
- $this->getConnection()->getPort(),
- $this->getId(),
- null,
- $password
- );
-
- try {
- $p4->login();
- return true;
- } catch (LoginException $e) {
- return false;
- }
- }
-
- /**
- * Get the type for this account. Expected to be one of 'service', 'operator' or 'standard'.
- *
- * @return the 'type' of this user, by default 'standard'
- */
- public function getType()
- {
- return $this->hasField('Type') && $this->getRawValue('Type')
- ? $this->getRawValue('Type')
- : static::STANDARD_USER;
- }
-
- /**
- * Set the user's type.
- *
- * @param string|null $type the type, expected to be one of 'service', 'operator' or 'standard'.
- * @return User provides fluent interface.
- * @throws \InvalidArgumentException if type field doesn't exist and a value other than null/standard is passed
- */
- public function setType($type)
- {
- $type = $type ?: 'standard';
- if (!$this->hasField('Type') && $type != static::STANDARD_USER) {
- throw new \InvalidArgumentException(
- 'The user spec lacks a Type field, setting to a value other than null or standard is not supported'
- );
- }
-
- return $this->setRawValue('Type', $type);
- }
-
- /**
- * Get an Iterator of all the Clients this user owns.
- *
- * @return FieldedIterator Iterator of Clients owned by current user
- * @throws Exception If no ID is set for this user
- */
- public function getClients()
- {
- if (!static::isValidId($this->getId())) {
- throw new Exception("Cannot get clients. No user id has been set.");
- }
-
- return Client::fetchAll(
- array(Client::FETCH_BY_OWNER => $this->getId()),
- $this->getConnection()
- );
- }
-
- /**
- * Get the names of groups that this user belongs to.
- *
- * @return FieldedIterator Iterator of Groups this user belongs to.
- */
- public function getGroups()
- {
- if (!static::isValidId($this->getId())) {
- throw new Exception("Cannot get groups. No user id has been set.");
- }
-
- return Group::fetchAll(
- array(Group::FETCH_BY_USER => $this->getId(), Group::FETCH_INDIRECT),
- $this->getConnection()
- );
- }
-
- /**
- * Add this user to the named group.
- *
- * @param string $group the name of the group to add the user to.
- * @return User provides fluent interface.
- */
- public function addToGroup($group)
- {
- $group = Group::fetch($group, $this->getConnection())
- ->addUser($this->getId())
- ->save();
-
- return $this;
- }
-
- /**
- * Get the user's full name.
- *
- * @return string|null the user's full name.
- */
- public function getFullName()
- {
- return $this->getRawValue('FullName');
- }
-
- /**
- * Set the user's full name.
- *
- * @param string|null $name the full name to give the user.
- * @return User provides fluent interface.
- * @throws \InvalidArgumentException if given name is not a string.
- */
- public function setFullName($name)
- {
- if ($name !== null && !is_string($name)) {
- throw new \InvalidArgumentException("Cannot set full name. Invalid type given.");
- }
- return $this->setRawValue('FullName', $name);
- }
-
- /**
- * Get the user's email address.
- *
- * @return string|null the user's email address.
- */
- public function getEmail()
- {
- return $this->getRawValue('Email');
- }
-
- /**
- * Set the user's email address. We don't require a valid email
- * address here because Perforce doesn't enforce one. If we did
- * then users with invalid emails would be innaccessible.
- *
- * @param string|null $email the email of the user.
- * @return User provides fluent interface.
- * @throws \InvalidArgumentException if given email is not a string.
- */
- public function setEmail($email)
- {
- if ($email !== null && !is_string($email)) {
- throw new \InvalidArgumentException("Cannot set email. Invalid type given.");
- }
- return $this->setRawValue("Email", $email);
- }
-
- /**
- * Get the user's job view (selects jobs for inclusion during changelist creation).
- *
- * @return string|null the user's job view.
- */
- public function getJobView()
- {
- return $this->getRawValue('JobView');
- }
-
- /**
- * Set the user's job view (selects jobs for inclusion during changelist creation).
- *
- * @param string|null $jobView the user's job view.
- * @return User provides fluent interface.
- * @throws \InvalidArgumentException if given job view is not a string.
- */
- public function setJobView($jobView)
- {
- if ($jobView !== null && !is_string($jobView)) {
- throw new \InvalidArgumentException("Cannot set job view. Invalid type given.");
- }
- return $this->setRawValue("JobView", $jobView);
- }
-
-
- /**
- * Get the reviews for this client (depot paths to notify user of changes to).
- *
- * @return array list of filespec strings.
- */
- public function getReviews()
- {
- return $this->getRawValue('Reviews') ?: array();
- }
-
- /**
- * Set the reviews for this user (depot paths to notify user of changes to).
- * Reviews is passed as an array of filespec strings.
- *
- * @param array $reviews Review entries - an array of filespec strings.
- * @return User provides a fluent interface.
- * @throws \InvalidArgumentException if reviews is not an array.
- */
- public function setReviews($reviews)
- {
- if (!is_array($reviews)) {
- throw new \InvalidArgumentException('Reviews must be passed as array.');
- }
-
- return $this->setRawValue('Reviews', $reviews);
- }
-
- /**
- * Get the last update time for this user spec.
- * This value is read only, no setUpdateTime function is provided.
- *
- * If this is a brand new spec, null will be returned in lieu of a time.
- *
- * @return string|null Date/Time of last update, formatted "2009/11/23 12:57:06" or null
- */
- public function getUpdateDateTime()
- {
- return $this->getRawValue('Update');
- }
-
- /**
- * Get the last access time for this user spec.
- * This value is read only, no setAccessTime function is provided.
- *
- * If this is a brand new spec, null will be returned in lieu of a time.
- *
- * @return string|null Date/Time of last access, formatted "2009/11/23 12:57:06" or null
- */
- public function getAccessDateTime()
- {
- return $this->getRawValue('Access');
- }
-
- /**
- * Check if automatic user creation is enabled.
- *
- * @param ConnectionInterface $connection optional - a specific connection to use.
- * @return bool true if auto user creation is enabled, false otherwise.
- * @throws Exception if we exceed the maximum number of unlikely usernames
- */
- public static function isAutoUserCreationEnabled(ConnectionInterface $connection = null)
- {
- // if no connection given, use default.
- $connection = $connection ?: static::getDefaultConnection();
-
- $port = $connection->getPort();
-
- // limit the number of 'unlikely' username lookups to 3.
- $maxLookups = 3;
- for ($i = 0; $i < $maxLookups; $i++) {
- // generate an unlikely user name.
- $username = md5(mt_rand());
-
- // try to run p4 users as the unlikely user
- // (perforce won't create an account for this lookup).
- try {
- $connection = Connection::factory($port, $username);
- $result = $connection->run('users', $username);
- } catch (CommandException $e) {
- return false;
- }
-
- // ensure unlikely user doesn't exist.
- if (!$result->getData()) {
- return true;
- }
- }
-
- throw new \Exception(
- "Failed to determine if auto user creation is enabled."
- . "Exceeded the maximum of $maxLookups 'unlikely' username lookups."
- );
- }
-
- /**
- * Produce set of flags for the spec list command, given fetch all options array.
- * Extends parent to add support for filter option.
- *
- * @param array $options array of options to augment fetch behavior.
- * see fetchAll for documented options.
- * @return array set of flags suitable for passing to spec list command.
- */
- protected static function getFetchAllFlags($options)
- {
- $flags = parent::getFetchAllFlags($options);
-
- // ensure we include service/operator users
- $flags[] = '-a';
-
- if (isset($options[static::FETCH_BY_NAME])) {
- $name = $options[static::FETCH_BY_NAME];
-
- if ((!is_array($name) || !count($name)) && (!is_string($name) || trim($name) === "")) {
- throw new \InvalidArgumentException(
- 'Filter by Name expects a non-empty string or an non-empty array as input'
- );
- }
-
- // if array is given, ensure values are non-empty strings
- if (is_array($name)) {
- $names = $name;
- $filtered = array_filter($names, 'is_string');
- $filtered = array_filter($filtered, 'trim');
-
- if (count($names) !== count($filtered)) {
- throw new \InvalidArgumentException(
- 'Filter by Name expects all names in the input array to be non-empty strings'
- );
- }
- $flags = array_merge($flags, $names);
- } else {
- $flags[] = $name;
- }
- }
-
- return $flags;
- }
-
- /**
- * Check if the given id is in a valid format for user specs.
- *
- * @param string $id the id to check
- * @return bool true if id is valid, false otherwise
- */
- protected static function isValidId($id)
- {
- $validator = new Validate\UserName;
- return $validator->isValid($id);
- }
-
- /**
- * Immediately set the user's password to the given password.
- * If the current password is given, it will be validated.
- *
- * @param string $newPassword the new password.
- * @param string $oldPassword optional - existing password.
- * @param ConnectionInterface $connection optional - a specific connection to use.
- * @return User provides fluent interface.
- * @throws Exception if the password can't be set.
- */
- protected function changePassword(
- $newPassword,
- $oldPassword = null,
- ConnectionInterface $connection = null
- ) {
- $input = array();
-
- // if caller supplied an old password, prepend it to input array.
- if ($oldPassword) {
- $input[] = $oldPassword;
- }
-
- // always confirm old password
- $input[] = $newPassword;
- $input[] = $newPassword;
-
- // if no connection given, use default.
- $connection = $connection ?: $this->getConnection();
-
- // if not connected as this user, supply user id.
- $flags = array();
- if ($connection->getUser() !== $this->getId()) {
- $flags[] = $this->getId();
- }
-
- // attempt to set password.
- $result = $connection->run("password", $flags, $input);
-
- // change connection credentials if password for connected user has been changed
- // if we don't do this automatically, subsequent commands will fail when using
- // the command-line connection, but would succeed using the P4PHP extension.
- if ($connection->getUser() === $this->getId()) {
- $connection->setPassword($newPassword);
- if ($connection->getTicket()) {
- $connection->login($connection->isTicketUnlocked());
- }
- }
-
- return $this;
- }
- }