* * @ORM\OneToMany(targetEntity="App\Entity\UserPreference", mappedBy="user", cascade={"persist"}) */ private $preferences; /** * List of all team memberships. * * @var Collection * * @Serializer\Expose() * @Serializer\Groups({"User_Entity"}) * @SWG\Property(ref="#/definitions/TeamMembership") * * @ORM\OneToMany(targetEntity="App\Entity\TeamMember", mappedBy="user", fetch="LAZY", cascade={"persist"}, orphanRemoval=true) * @ORM\JoinColumn(onDelete="CASCADE") * @Assert\NotNull() */ private $memberships; /** * The type of authentication used by the user (e.g. "kimai", "ldap", "saml") * * @var string|null * @internal for internal usage only * * @ORM\Column(name="auth", type="string", length=20, nullable=true) * @Assert\Length(max=20) */ private $auth = self::AUTH_INTERNAL; /** * This flag will be initialized in UserEnvironmentSubscriber. * * @var bool|null * @internal has no database mapping as the value is calculated from a permission */ private $isAllowedToSeeAllData = null; /** * @Serializer\Expose() * @Serializer\Groups({"Default"}) * * @var string * @ORM\Column(name="username", type="string", length=180) * @Assert\NotBlank(groups={"Registration", "UserCreate", "Profile"}) * @Assert\Length(min="2", max="60", groups={"Registration", "UserCreate", "Profile"}) */ private $username; /** * @var string * @ORM\Column(name="email", type="string", length=180) * @Assert\NotBlank(groups={"Registration", "UserCreate", "Profile"}) * @Assert\Length(min="2", max="180") * @Assert\Email(groups={"Registration", "UserCreate", "Profile"}) */ private $email; /** * @Serializer\Expose() * @Serializer\Groups({"Default"}) * * @Exporter\Expose(label="label.account_number") * * @var string|null * @ORM\Column(name="account", type="string", length=30, nullable=true) * @Assert\Length(allowEmptyString=true, max="30", groups={"Registration", "UserCreate", "Profile"}) */ private $accountNumber; /** * @Serializer\Expose() * @Serializer\Groups({"Default"}) * * @var bool * @ORM\Column(name="enabled", type="boolean") */ private $enabled = false; /** * Encrypted password. Must be persisted. * * @var string * @ORM\Column(name="password", type="string") */ private $password; /** * Plain password. Used for model validation, not persisted. * * TODO make the password rules configurable * * @var string|null * @Assert\NotBlank(groups={"Registration", "PasswordUpdate", "UserCreate"}) * @Assert\Length(min="8", max="60", groups={"Registration", "PasswordUpdate", "UserCreate", "ResetPassword", "ChangePassword"}) */ private $plainPassword; /** * @var \DateTime|null * @ORM\Column(name="last_login", type="datetime", nullable=true) */ private $lastLogin; /** * Random string sent to the user email address in order to verify it. * * @var string|null * @ORM\Column(name="confirmation_token", type="string", length=180, unique=true, nullable=true) */ private $confirmationToken; /** * @var \DateTime|null * @ORM\Column(name="password_requested_at", type="datetime", nullable=true) */ private $passwordRequestedAt; /** * List of all role names * * @Serializer\Expose() * @Serializer\Groups({"User_Entity"}) * @Serializer\Type("array") * * @var array * @ORM\Column(name="roles", type="array") * @Constraints\Role(groups={"RolesUpdate"}) */ private $roles = []; use ColorTrait; public function __construct() { $this->registeredAt = new DateTime(); $this->preferences = new ArrayCollection(); $this->memberships = new ArrayCollection(); } public function getId(): ?int { return $this->id; } public function getRegisteredAt(): ?DateTime { return $this->registeredAt; } public function setRegisteredAt(DateTime $registeredAt): User { $this->registeredAt = $registeredAt; return $this; } public function setAlias(?string $alias): User { $this->alias = StringHelper::ensureMaxLength($alias, 60); return $this; } public function getAlias(): ?string { return $this->alias; } public function getTitle(): ?string { return $this->title; } public function setTitle(?string $title): User { $this->title = StringHelper::ensureMaxLength($title, 50); return $this; } public function getAvatar(): ?string { return $this->avatar; } public function setAvatar(?string $avatar): User { $this->avatar = $avatar; return $this; } public function getApiToken(): ?string { return $this->apiToken; } public function setApiToken(?string $apiToken): User { $this->apiToken = $apiToken; return $this; } public function getPlainApiToken(): ?string { return $this->plainApiToken; } public function setPlainApiToken(?string $plainApiToken): User { $this->plainApiToken = $plainApiToken; return $this; } /** * Read-only list of of all visible user preferences. * * @Serializer\VirtualProperty * @Serializer\SerializedName("preferences"), * @Serializer\Groups({"User_Entity"}) * @SWG\Property(type="array", @SWG\Items(ref="#/definitions/UserPreference")) * * @internal only for API usage * @return UserPreference[] */ public function getVisiblePreferences(): array { // hide all internal preferences, which are either available in other fields // or which are only used within the Kimai UI $skip = [ UserPreference::TIMEZONE, UserPreference::LOCALE, UserPreference::SKIN, 'calendar.initial_view', 'login.initial_view', 'reporting.initial_view', 'theme.collapsed_sidebar', 'theme.layout', 'theme.update_browser_title', 'timesheet.daily_stats', 'timesheet.export_decimal', ]; $all = []; foreach ($this->preferences as $preference) { if ($preference->isEnabled() && !\in_array($preference->getName(), $skip)) { $all[] = $preference; } } return $all; } /** * @return Collection */ public function getPreferences(): Collection { return $this->preferences; } /** * @param iterable $preferences * @return User */ public function setPreferences(iterable $preferences): User { $this->preferences = new ArrayCollection(); foreach ($preferences as $preference) { $this->addPreference($preference); } return $this; } /** * @param string $name * @param bool|int|string|null $value */ public function setPreferenceValue(string $name, $value = null) { $pref = $this->getPreference($name); if (null === $pref) { $pref = new UserPreference(); $pref->setName($name); $this->addPreference($pref); } $pref->setValue($value); } public function getPreference(string $name): ?UserPreference { // this code will be triggered, if a currently logged-in user will be deleted and then refreshed from the session // via one of the UserProvider - e.g. see LdapUserProvider::refreshUser() which calls $user->getPreferenceValue() if ($this->preferences === null) { return null; } foreach ($this->preferences as $preference) { if ($preference->getName() === $name) { return $preference; } } return null; } public function getTimeFormat(): string { if ($this->is24Hour()) { return 'H:i'; } return 'h:i A'; } public function is24Hour(): bool { return (bool) $this->getPreferenceValue(UserPreference::HOUR_24, true); } public function getLocale(): string { return $this->getPreferenceValue(UserPreference::LOCALE, User::DEFAULT_LANGUAGE); } public function getTimezone(): string { return $this->getPreferenceValue(UserPreference::TIMEZONE, date_default_timezone_get()); } public function getLanguage(): string { return $this->getLocale(); } public function setLanguage(?string $language) { if ($language === null) { $language = User::DEFAULT_LANGUAGE; } $this->setPreferenceValue(UserPreference::LOCALE, $language); } public function isFirstDayOfWeekSunday(): bool { return $this->getFirstDayOfWeek() === 'sunday'; } public function getFirstDayOfWeek(): string { return $this->getPreferenceValue(UserPreference::FIRST_WEEKDAY, User::DEFAULT_FIRST_WEEKDAY); } public function isSmallLayout(): bool { return $this->getPreferenceValue('theme.layout', 'fixed') === 'boxed'; } public function isExportDecimal(): bool { return (bool) $this->getPreferenceValue('timesheet.export_decimal', false); } public function setTimezone(?string $timezone) { if ($timezone === null) { $timezone = date_default_timezone_get(); } $this->setPreferenceValue(UserPreference::TIMEZONE, $timezone); } /** * @param string $name * @param mixed $default * @return bool|int|string|null */ public function getPreferenceValue(string $name, $default = null) { $preference = $this->getPreference($name); if (null === $preference) { return $default; } return $preference->getValue(); } /** * @param string $name * @return bool|int|string|null */ public function getMetaFieldValue(string $name) { return $this->getPreferenceValue($name); } /** * @param UserPreference $preference * @return User */ public function addPreference(UserPreference $preference): User { if (null === $this->preferences) { $this->preferences = new ArrayCollection(); } $this->preferences->add($preference); $preference->setUser($this); return $this; } public function addMembership(TeamMember $member): void { if ($this->memberships->contains($member)) { return; } if ($member->getUser() === null) { $member->setUser($this); } if ($member->getUser() !== $this) { throw new \InvalidArgumentException('Cannot set foreign user membership'); } // when using the API an invalid Team ID triggers the validation too late if ($member->getTeam() === null) { return; } if (null !== $this->findMemberByTeam($member->getTeam())) { return; } $this->memberships->add($member); $member->getTeam()->addMember($member); } private function findMemberByTeam(Team $team): ?TeamMember { foreach ($this->memberships as $member) { if ($member->getTeam() === $team) { return $member; } } return null; } public function removeMembership(TeamMember $member): void { if (!$this->memberships->contains($member)) { return; } $this->memberships->removeElement($member); $member->getTeam()->removeMember($member); $member->setUser(null); $member->setTeam(null); } /** * @return Collection */ public function getMemberships(): Collection { return $this->memberships; } public function hasMembership(TeamMember $member): bool { return $this->memberships->contains($member); } /** * Checks if the user is member of any team. * * @return bool */ public function hasTeamAssignment(): bool { return !$this->memberships->isEmpty(); } /** * Checks is the user is teamlead in any of the assigned teams. * * @see User::hasTeamleadRole() * @return bool */ public function isTeamlead(): bool { foreach ($this->memberships as $membership) { if ($membership->isTeamlead()) { return true; } } return false; } /** * Checks if the given user is a team member. * * @param User $user * @return bool */ public function hasTeamMember(User $user): bool { foreach ($this->memberships as $membership) { if ($membership->getTeam()->hasUser($user)) { return true; } } return false; } /** * List of all teams, this user is part of * * @Serializer\VirtualProperty * @Serializer\SerializedName("teams"), * @Serializer\Groups({"User_Entity"}) * @SWG\Property(type="array", @SWG\Items(ref="#/definitions/Team")) * * @return Team[] */ public function getTeams(): iterable { $teams = []; foreach ($this->memberships as $membership) { $teams[] = $membership->getTeam(); } return $teams; } /** * Required in the User profile screen to edit his teams. * * @param Team $team */ public function addTeam(Team $team): void { foreach ($this->memberships as $membership) { if ($membership->getTeam() === $team) { return; } } $membership = new TeamMember(); $membership->setUser($this); $membership->setTeam($team); $this->addMembership($membership); } /** * Required in the User profile screen to edit his teams. * * @param Team $team */ public function removeTeam(Team $team): void { foreach ($this->memberships as $membership) { if ($membership->getTeam() === $team) { $this->removeMembership($membership); return; } } } public function isInTeam(Team $team): bool { foreach ($this->memberships as $membership) { if ($membership->getTeam() === $team) { return true; } } return false; } public function isTeamleadOf(Team $team): bool { if (null !== ($member = $this->findMemberByTeam($team))) { return $member->isTeamlead(); } return false; } public function canSeeAllData(): bool { return $this->isSuperAdmin() || true === $this->isAllowedToSeeAllData; } /** * This method should not be called by plugins and returns true on success or false on a failure. * * @internal immutable property that cannot be set by plugins * @param bool $canSeeAllData * @return bool * @throws Exception */ public function initCanSeeAllData(bool $canSeeAllData): bool { // prevent manipulation from plugins if (null !== $this->isAllowedToSeeAllData) { return false; } $this->isAllowedToSeeAllData = $canSeeAllData; return true; } public function hasTeamleadRole(): bool { return $this->hasRole(static::ROLE_TEAMLEAD); } public function isAdmin(): bool { return $this->hasRole(static::ROLE_ADMIN); } public function getDisplayName(): ?string { if (!empty($this->getAlias())) { return $this->getAlias(); } return $this->getUsername(); } public function getAuth(): ?string { return $this->auth; } public function setAuth(string $auth): User { $this->auth = $auth; return $this; } public function isSamlUser(): bool { return $this->auth === self::AUTH_SAML; } public function isLdapUser(): bool { return $this->auth === self::AUTH_LDAP; } public function isInternalUser(): bool { return $this->auth === null || $this->auth === self::AUTH_INTERNAL; } public function addRole(string $role) { $role = strtoupper($role); if ($role === static::DEFAULT_ROLE) { return $this; } if (!\in_array($role, $this->roles, true)) { $this->roles[] = $role; } return $this; } /** * {@inheritdoc} */ public function eraseCredentials() { $this->plainPassword = null; $this->plainApiToken = null; } /** * {@inheritdoc} */ public function getUsername() { return $this->username; } /** * {@inheritdoc} */ public function getEmail() { return $this->email; } /** * {@inheritdoc} */ public function getPassword() { return $this->password; } public function getPlainPassword(): ?string { return $this->plainPassword; } public function getLastLogin(): ?DateTime { if ($this->lastLogin !== null) { // make sure to use the users own timezone $this->lastLogin->setTimeZone(new \DateTimeZone($this->getTimezone())); } return $this->lastLogin; } public function getConfirmationToken(): ?string { return $this->confirmationToken; } /** * {@inheritdoc} */ public function getRoles() { $roles = $this->roles; // we need to make sure to have at least one role $roles[] = static::DEFAULT_ROLE; return array_values(array_unique($roles)); } public function hasRole($role): bool { return \in_array(strtoupper($role), $this->getRoles(), true); } public function setSuperAdmin(bool $isSuper): void { if (true === $isSuper) { $this->addRole(static::ROLE_SUPER_ADMIN); } else { $this->removeRole(static::ROLE_SUPER_ADMIN); } } public function isSuperAdmin(): bool { return $this->hasRole(static::ROLE_SUPER_ADMIN); } public function removeRole($role): User { if (false !== $key = array_search(strtoupper($role), $this->roles, true)) { unset($this->roles[$key]); $this->roles = array_values($this->roles); } return $this; } public function setUsername($username): User { $this->username = $username; return $this; } public function setEmail($email): User { $this->email = $email; return $this; } public function isEnabled(): bool { return $this->enabled; } public function setEnabled(bool $enabled): User { $this->enabled = $enabled; return $this; } public function setPassword($password): User { $this->password = $password; return $this; } public function setPlainPassword($password): User { $this->plainPassword = $password; return $this; } public function setLastLogin(\DateTime $time = null): User { $this->lastLogin = $time; return $this; } public function setConfirmationToken($confirmationToken): User { $this->confirmationToken = $confirmationToken; return $this; } public function setPasswordRequestedAt(\DateTime $date = null): User { $this->passwordRequestedAt = $date; return $this; } /** * Gets the timestamp that the user requested a password reset. * * @return DateTime|null */ public function getPasswordRequestedAt(): ?DateTime { return $this->passwordRequestedAt; } public function isPasswordRequestNonExpired(int $seconds): bool { $date = $this->getPasswordRequestedAt(); if ($date === null || !($date instanceof DateTime)) { return false; } return $date->getTimestamp() + $seconds > time(); } public function setRoles(array $roles): User { $this->roles = []; foreach ($roles as $role) { $this->addRole($role); } return $this; } public function isEqualTo(UserInterface $user) { if (!$user instanceof self) { return false; } if ($this->password !== $user->getPassword()) { return false; } if ($this->username !== $user->getUsername()) { return false; } return true; } public function __serialize(): array { return [ 'id' => $this->id, 'username' => $this->username, 'enabled' => $this->enabled, 'email' => $this->email, 'password' => $this->password, ]; } /** * {@inheritdoc} */ public function serialize() { return serialize([ $this->password, $this->username, $this->enabled, $this->id, $this->email, ]); } public function __unserialize(array $data): void { if (!\array_key_exists('id', $data)) { return; } $this->id = $data['id']; $this->username = $data['username']; $this->enabled = $data['enabled']; $this->email = $data['email']; $this->password = $data['password']; } /** * {@inheritdoc} */ public function unserialize($serialized) { $data = unserialize($serialized); // unserialize a user object from <= 1.14 if (8 === \count($data)) { unset($data[1], $data[2], $data[7]); $data = array_values($data); } list( $this->password, $this->username, $this->enabled, $this->id, $this->email) = $data; } /** * {@inheritdoc} */ public function getSalt() { return null; } /** * @return string */ public function __toString() { return $this->getDisplayName(); } public function getInitials(): string { $length = 2; $name = $this->getDisplayName(); $initial = ''; if (filter_var($name, FILTER_VALIDATE_EMAIL)) { // turn my.email@gmail.com into "My Email" $result = mb_strstr($name, '@', true); $name = $result === false ? $name : $result; $name = str_replace('.', ' ', $name); } $words = explode(' ', $name); // if name contains single word, use first N character if (\count($words) === 1) { $initial = $words[0]; if (mb_strlen($name) >= $length) { $initial = mb_substr($name, 0, $length, 'UTF-8'); } } else { // otherwise, use initial char from each word foreach ($words as $word) { $initial .= mb_substr($word, 0, 1, 'UTF-8'); } $initial = mb_substr($initial, 0, $length, 'UTF-8'); } $initial = mb_strtoupper($initial); return $initial; } public function getAccountNumber(): ?string { return $this->accountNumber; } public function setAccountNumber(?string $accountNumber): void { $this->accountNumber = $accountNumber; } public function isSystemAccount(): bool { return false; } }