. */ namespace Xibo\Entity; use League\OAuth2\Server\Entities\UserEntityInterface; use Respect\Validation\Validator as v; use Xibo\Factory\ApplicationScopeFactory; use Xibo\Factory\CampaignFactory; use Xibo\Factory\DataSetFactory; use Xibo\Factory\DayPartFactory; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; use Xibo\Factory\LayoutFactory; use Xibo\Factory\MediaFactory; use Xibo\Factory\PermissionFactory; use Xibo\Factory\PlayerVersionFactory; use Xibo\Factory\PlaylistFactory; use Xibo\Factory\ScheduleFactory; use Xibo\Factory\UserFactory; use Xibo\Factory\UserGroupFactory; use Xibo\Factory\UserOptionFactory; use Xibo\Factory\WidgetFactory; use Xibo\Helper\Pbkdf2Hash; use Xibo\Service\ConfigServiceInterface; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; use Xibo\Support\Exception\AccessDeniedException; use Xibo\Support\Exception\DuplicateEntityException; use Xibo\Support\Exception\GeneralException; use Xibo\Support\Exception\InvalidArgumentException; use Xibo\Support\Exception\LibraryFullException; use Xibo\Support\Exception\NotFoundException; /** * Class User * @package Xibo\Entity * * @SWG\Definition() */ class User implements \JsonSerializable, UserEntityInterface { use EntityTrait; /** * @SWG\Property(description="The ID of this User") * @var int */ public $userId; /** * @SWG\Property(description="The user name") * @var string */ public $userName; /** * @SWG\Property(description="The user type ID") * @var int */ public $userTypeId; /** * @SWG\Property(description="Flag indicating whether this user is logged in or not") * @var int */ public $loggedIn; /** * @SWG\Property(description="Email address of the user used for email alerts") * @var string */ public $email; /** * @SWG\Property(description="The pageId of the Homepage for this User") * @var int */ public $homePageId; /** * @SWG\Property(description="This users home folder") * @var int */ public $homeFolderId; /** * @SWG\Property(description="A timestamp indicating the time the user last logged into the CMS") * @var int */ public $lastAccessed; /** * @SWG\Property(description="A flag indicating whether this user has see the new user wizard") * @var int */ public $newUserWizard = 0; /** * @SWG\Property(description="A flag indicating whether the user is retired") * @var int */ public $retired; private $CSPRNG; private $password; /** * @SWG\Property(description="A flag indicating whether password change should be forced for this user") * @var int */ public $isPasswordChangeRequired = 0; /** * @SWG\Property(description="The users user group ID") * @var int */ public $groupId; /** * @SWG\Property(description="The users group name") * @var int */ public $group; /** * @SWG\Property(description="The users library quota in bytes") * @var int */ public $libraryQuota; /** * @SWG\Property(description="First Name") * @var string */ public $firstName; /** * @SWG\Property(description="Last Name") * @var string */ public $lastName; /** * @SWG\Property(description="Phone Number") * @var string */ public $phone; /** * @SWG\Property(description="Reference field 1") * @var string */ public $ref1; /** * @SWG\Property(description="Reference field 2") * @var string */ public $ref2; /** * @SWG\Property(description="Reference field 3") * @var string */ public $ref3; /** * @SWG\Property(description="Reference field 4") * @var string */ public $ref4; /** * @SWG\Property(description="Reference field 5") * @var string */ public $ref5; /** * @SWG\Property(description="An array of user groups this user is assigned to") * @var UserGroup[] */ public $groups = []; /** * @SWG\Property(description="An array of Campaigns for this User") * @var Campaign[] */ public $campaigns = []; /** * @SWG\Property(description="An array of Layouts for this User") * @var Layout[] */ public $layouts = []; /** * @SWG\Property(description="An array of Media for this user") * @var Media[] */ public $media = []; /** * @SWG\Property(description="An array of Scheduled Events for this User") * @var Schedule[] */ public $events = []; /** * @SWG\Property(description="An array of Playlists owned by this User") * @var Playlist[] */ public $playlists = []; /** * @SWG\Property(description="An array of Display Groups owned by this User") * @var DisplayGroup[] */ public $displayGroups = []; /** * @SWG\Property(description="An array of Dayparts owned by this User") * @var DayPart[] */ public $dayParts = []; /** * @SWG\Property(description="Does this User receive system notifications.") * @var int */ public $isSystemNotification = 0; /** * @SWG\Property(description="Does this User receive system notifications.") * @var int */ public $isDisplayNotification = 0; /** * @SWG\Property(description="Does this User receive DataSet notifications.") * @var int */ public $isDataSetNotification = 0; /** * @SWG\Property(description="Does this User receive Layout notifications.") * @var int */ public $isLayoutNotification = 0; /** * @SWG\Property(description="Does this User receive Library notifications.") * @var int */ public $isLibraryNotification = 0; /** * @SWG\Property(description="Does this User receive Report notifications.") * @var int */ public $isReportNotification = 0; /** * @SWG\Property(description="Does this User receive Schedule notifications.") * @var int */ public $isScheduleNotification = 0; /** * @SWG\Property(description="Does this User receive Custom notifications.") * @var int */ public $isCustomNotification = 0; /** * @SWG\Property(description="The two factor type id") * @var int */ public $twoFactorTypeId; /** * @SWG\Property(description="Two Factor authorisation shared secret for this user") * @var string */ public $twoFactorSecret; /** * @SWG\Property(description="Two Factor authorisation recovery codes", @SWG\Items(type="string")) * @var array */ public $twoFactorRecoveryCodes = []; /** * @var UserOption[] */ private $userOptions = []; /** * User options that have been removed * @var \Xibo\Entity\UserOption[] */ private $userOptionsRemoved = []; /** @var array Resolved Features for the User and their Groups */ private $resolvedFeatures = null; /** * Cached Permissions * @var array[Permission] */ private $permissionCache = array(); /** * Cached Page Permissions * @var array[Page] */ private $pagePermissionCache = null; /** * @var ConfigServiceInterface */ private $configService; /** * @var UserFactory */ private $userFactory; /** * @var UserGroupFactory */ private $userGroupFactory; /** * @var PermissionFactory */ private $permissionFactory; /** * @var CampaignFactory */ private $campaignFactory; /** * @var LayoutFactory */ private $layoutFactory; /** * @var MediaFactory */ private $mediaFactory; /** * @var ScheduleFactory */ private $scheduleFactory; /** * @var UserOptionFactory */ private $userOptionFactory; /** @var DisplayFactory */ private $displayFactory; /** @var ApplicationScopeFactory */ private $applicationScopeFactory; /** @var DisplayGroupFactory */ private $displayGroupFactory; /** @var WidgetFactory */ private $widgetFactory; /** @var PlayerVersionFactory */ private $playerVersionFactory; /** @var PlaylistFactory */ private $playlistFactory; /** @var DataSetFactory */ private $dataSetFactory; /** @var DayPartFactory */ private $dayPartFactory; /** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher * @param ConfigServiceInterface $configService * @param UserFactory $userFactory * @param PermissionFactory $permissionFactory * @param UserOptionFactory $userOptionFactory * @param ApplicationScopeFactory $applicationScopeFactory */ public function __construct( $store, $log, $dispatcher, $configService, $userFactory, $permissionFactory, $userOptionFactory, $applicationScopeFactory ) { $this->setCommonDependencies($store, $log, $dispatcher); $this->configService = $configService; $this->userFactory = $userFactory; $this->permissionFactory = $permissionFactory; $this->userOptionFactory = $userOptionFactory; $this->applicationScopeFactory = $applicationScopeFactory; $this->excludeProperty('twoFactorSecret'); $this->excludeProperty('twoFactorRecoveryCodes'); } /** * Set the user group factory * @param UserGroupFactory $userGroupFactory * @return $this */ public function setChildAclDependencies($userGroupFactory) { // Assert myself on these factories $userGroupFactory->setAclDependencies($this, $this->userFactory); $this->userFactory->setAclDependencies($this, $this->userFactory); $this->userGroupFactory = $userGroupFactory; return $this; } /** * @return string */ public function __toString() { return sprintf( 'User %s. userId: %d, UserTypeId: %d, homePageId: %d, email = %s', $this->userName, $this->userId, $this->userTypeId, $this->homePageId, $this->email ); } /** * @return string */ private function hash() { return md5(json_encode($this)); } /** * @return int */ public function getOwnerId() { return $this->getId(); } /** * @return int */ public function getId() { return $this->userId; } /** @inheritDoc */ public function getIdentifier() { return $this->userId; } /** * Get Option * @param string $option * @return UserOption * @throws NotFoundException */ public function getOption($option) { $this->load(); foreach ($this->userOptions as $userOption) { /* @var UserOption $userOption */ if ($userOption->option == $option) { return $userOption; } } $this->getLog()->debug(sprintf('UserOption %s not found', $option)); throw new NotFoundException(__('User Option not found')); } /** * Remove the provided option * @param \Xibo\Entity\UserOption $option * @return $this */ private function removeOption($option) { $this->getLog()->debug('Removing: ' . $option); $this->userOptionsRemoved[] = $option; $this->userOptions = array_diff($this->userOptions, [$option]); return $this; } /** * Get User Option Value * @param string $option * @param mixed $default * @return mixed * @throws NotFoundException */ public function getOptionValue($option, $default) { $this->load(); try { $userOption = $this->getOption($option); return $userOption->value; } catch (NotFoundException $e) { $this->getLog()->debug('Returning the default value: ' . var_export($default, true)); return $default; } } /** * Set User Option Value * @param string $option * @param mixed $value */ public function setOptionValue($option, $value) { try { $option = $this->getOption($option); if ($value === null) { $this->removeOption($option); } else { $option->value = $value; } } catch (NotFoundException $e) { $this->userOptions[] = $this->userOptionFactory->create($this->userId, $option, $value); } } /** * Remove all user options by a prefix * @param string $optionPrefix The option prefix * @return $this * @throws \Xibo\Support\Exception\NotFoundException */ public function removeOptionByPrefix(string $optionPrefix) { $this->load(); foreach ($this->userOptions as $userOption) { if (str_starts_with($userOption->option, $optionPrefix)) { $this->removeOption($userOption); } } return $this; } /** * Set a new password * @param string $password * @param null $oldPassword * @throws GeneralException */ public function setNewPassword($password, $oldPassword = null) { // Validate the old password if one is provided if ($oldPassword != null) { $this->checkPassword($oldPassword); } // Basic validation if (!v::stringType()->notEmpty()->validate($password)) { throw new InvalidArgumentException(__('Please enter a Password.'), 'password'); } // Test against a policy if one exists $this->testPasswordAgainstPolicy($password); // Set the hash $this->setNewPasswordHash($password); } /** * Set a new password and hash * @param string $password */ private function setNewPasswordHash($password) { $this->password = password_hash($password, PASSWORD_DEFAULT); $this->CSPRNG = 2; } /** * Check password * @param string $password * @throws AccessDeniedException if the passwords don't match * @throws DuplicateEntityException * @throws InvalidArgumentException * @throws NotFoundException if the user has not been loaded */ public function checkPassword($password) { if ($this->userId == 0) { throw new NotFoundException(__('User not found')); } if ($this->CSPRNG == 0) { // Password is tested using a plain MD5 check if ($this->password != md5($password)) { throw new AccessDeniedException(); } } else if ($this->CSPRNG == 1) { // Test with Pbkdf2 try { if (!Pbkdf2Hash::verifyPassword($password, $this->password)) { $this->getLog()->debug('Password failed Pbkdf2Hash Check.'); throw new AccessDeniedException(); } } catch (\InvalidArgumentException $e) { $this->getLog()->warning('Invalid password hash stored for userId ' . $this->userId); $this->getLog()->debug('Hash error: ' . $e->getMessage()); } } else { if (!password_verify($password, $this->password)) { $this->getLog()->debug('Password failed Hash Check.'); throw new AccessDeniedException(); } } $this->getLog()->debug('Password checked out OK'); // Do we need to convert? $this->updateHashIfRequired($password); } /** * Update hash if required * @param string $password * @throws DuplicateEntityException * @throws InvalidArgumentException */ private function updateHashIfRequired($password) { if (($this->CSPRNG == 0 || $this->CSPRNG == 1) || ($this->CSPRNG == 2 && password_needs_rehash($this->password, PASSWORD_DEFAULT))) { $this->getLog()->debug('Converting password to use latest hash'); // Set the hash $this->setNewPasswordHash($password); // Save $this->save(['validate' => false, 'passwordUpdate' => true]); } } /** * Check to see if a user id is in the session information * @return bool */ public function hasIdentity() { $userId = isset($_SESSION['userid']) ? intval($_SESSION['userid']) : 0; // Checks for a user ID in the session variable if ($userId == 0) { unset($_SESSION['userid']); return false; } else { $this->userId = $userId; return true; } } /** * Load this User * @param bool $all Load everything this user owns * @throws NotFoundException */ public function load($all = false) { if ($this->userId == null || $this->loaded) return; if ($this->userGroupFactory == null) { throw new \RuntimeException('Cannot load user without first calling setUserGroupFactory'); } $this->getLog()->debug(sprintf('Loading %d. All Objects = %d', $this->userId, $all)); $this->groups = $this->userGroupFactory->getByUserId($this->userId); $this->userOptions = $this->userOptionFactory->getByUserId($this->userId); // Set the hash $this->hash = $this->hash(); $this->loaded = true; } /** * Validate * @throws DuplicateEntityException * @throws InvalidArgumentException */ public function validate() { $this->getLog()->debug('Validate User'); if (!v::alnum('_.-')->length(1, 50)->validate($this->userName) && !v::email()->validate($this->userName)) throw new InvalidArgumentException(__('User name must be between 1 and 50 characters.'), 'userName'); if (!empty($this->libraryQuota) && !v::intType()->validate($this->libraryQuota)) throw new InvalidArgumentException(__('Library Quota must be a whole number.'), 'libraryQuota'); if (!empty($this->email) && !v::email()->validate($this->email)) throw new InvalidArgumentException(__('Please enter a valid email address or leave it empty.'), 'email'); try { $user = $this->userFactory->getByName($this->userName); if ($this->userId == null || $this->userId != $user->userId) throw new DuplicateEntityException(__('There is already a user with this name. Please choose another.')); } catch (NotFoundException $ignored) {} // System User if ($this->userId == $this->configService->getSetting('SYSTEM_USER') && $this->userTypeId != 1) { throw new InvalidArgumentException(__('This User is set as System User and needs to be super admin'), 'userId'); } if ($this->userId == $this->configService->getSetting('SYSTEM_USER') && $this->retired === 1) { throw new InvalidArgumentException(__('This User is set as System User and cannot be retired'), 'userId'); } // Library quota if (!empty($this->libraryQuota) && $this->libraryQuota < 0) { throw new InvalidArgumentException(__('Library Quota must be a positive number.'), 'libraryQuota'); } } /** * Save User * @param array $options * @throws DuplicateEntityException * @throws InvalidArgumentException * @throws NotFoundException */ public function save($options = []) { $options = array_merge([ 'validate' => true, 'passwordUpdate' => false, 'saveUserOptions' => true ], $options); if ($options['validate']) { $this->validate(); } $this->getLog()->debug('Saving user. ' . $this); if ($this->userId == 0) { $this->add(); $this->audit($this->userId, 'New user added', ['userName' => $this->userName]); } else if ($options['passwordUpdate']) { $this->updatePassword(); $this->audit($this->userId, 'User updated password', false); } else if ($this->hash() != $this->hash || $this->hasPropertyChanged('twoFactorRecoveryCodes') || $this->hasPropertyChanged('password') ) { $this->update(); $this->audit($this->userId, 'User updated'); } // Save user options if ($options['saveUserOptions']) { // Remove any that have been cleared foreach ($this->userOptionsRemoved as $userOption) { $userOption->delete(); } // Save all Options foreach ($this->userOptions as $userOption) { $userOption->userId = $this->userId; $userOption->save(); } } } /** * Delete User * @throws \Xibo\Support\Exception\GeneralException */ public function delete() { $this->getLog()->debug(sprintf('Deleting %d', $this->userId)); // We must ensure everything is loaded before we delete if ($this->hash == null) { $this->load(true); } // Remove the user specific group $group = $this->userGroupFactory->getById($this->groupId); $group->delete(); // Delete all user options foreach ($this->userOptions as $userOption) { /* @var RegionOption $userOption */ $userOption->delete(); } // Remove any assignments to groups foreach ($this->groups as $group) { $group->unassignUser($this); $group->save(['validate' => false]); } $this->getStore()->update('DELETE FROM `user` WHERE userId = :userId', ['userId' => $this->userId]); $this->audit($this->userId, 'User deleted', false); } /** * Add user */ private function add() { $sql = 'INSERT INTO `user` (UserName, UserPassword, isPasswordChangeRequired, usertypeid, newUserWizard, email, homePageId, homeFolderId, CSPRNG, firstName, lastName, phone, ref1, ref2, ref3, ref4, ref5) VALUES (:userName, :password, :isPasswordChangeRequired, :userTypeId, :newUserWizard, :email, :homePageId, :homeFolderId, :CSPRNG, :firstName, :lastName, :phone, :ref1, :ref2, :ref3, :ref4, :ref5)'; // Get the ID of the record we just inserted $this->userId = $this->getStore()->insert($sql, [ 'userName' => $this->userName, 'password' => $this->password, 'isPasswordChangeRequired' => $this->isPasswordChangeRequired, 'userTypeId' => $this->userTypeId, 'newUserWizard' => $this->newUserWizard, 'email' => $this->email, 'homePageId' => $this->homePageId, 'homeFolderId' => $this->homeFolderId, 'CSPRNG' => $this->CSPRNG, 'firstName' => $this->firstName, 'lastName' => $this->lastName, 'phone' => $this->phone, 'ref1' => $this->ref1, 'ref2' => $this->ref2, 'ref3' => $this->ref3, 'ref4' => $this->ref4, 'ref5' => $this->ref5 ]); // Add the user group $group = $this->userGroupFactory->create($this->userName, $this->libraryQuota); $group->setOwner($this); $group->isSystemNotification = $this->isSystemNotification; $group->isDisplayNotification = $this->isDisplayNotification; $group->isCustomNotification = $this->isCustomNotification; $group->isDataSetNotification = $this->isDataSetNotification; $group->isLayoutNotification = $this->isLayoutNotification; $group->isLibraryNotification = $this->isLibraryNotification; $group->isReportNotification = $this->isReportNotification; $group->isScheduleNotification = $this->isScheduleNotification; $group->save(); // Assert the groupIds on the user (we do this so we have group in the API return) $this->groupId = $group->getId(); $this->group = $group->group; } /** * Update user * @throws DuplicateEntityException * @throws InvalidArgumentException * @throws NotFoundException */ private function update() { $this->getLog()->debug('Update userId ' . $this->userId); $sql = 'UPDATE `user` SET UserName = :userName, homePageId = :homePageId, homeFolderId = :homeFolderId, Email = :email, Retired = :retired, userTypeId = :userTypeId, newUserWizard = :newUserWizard, CSPRNG = :CSPRNG, `UserPassword` = :password, `isPasswordChangeRequired` = :isPasswordChangeRequired, `twoFactorTypeId` = :twoFactorTypeId, `twoFactorSecret` = :twoFactorSecret, `twoFactorRecoveryCodes` = :twoFactorRecoveryCodes, `firstName` = :firstName, `lastName` = :lastName, `phone` = :phone, `ref1` = :ref1, `ref2` = :ref2, `ref3` = :ref3, `ref4` = :ref4, `ref5` = :ref5 WHERE userId = :userId'; $params = array( 'userName' => $this->userName, 'userTypeId' => $this->userTypeId, 'email' => $this->email, 'homePageId' => $this->homePageId, 'homeFolderId' => $this->homeFolderId, 'retired' => $this->retired, 'newUserWizard' => $this->newUserWizard, 'CSPRNG' => $this->CSPRNG, 'password' => $this->password, 'isPasswordChangeRequired' => $this->isPasswordChangeRequired, 'twoFactorTypeId' => $this->twoFactorTypeId, 'twoFactorSecret' => $this->twoFactorSecret, 'twoFactorRecoveryCodes' => ($this->twoFactorRecoveryCodes == '') ? null : json_encode($this->twoFactorRecoveryCodes), 'firstName' => $this->firstName, 'lastName' => $this->lastName, 'phone' => $this->phone, 'ref1' => $this->ref1, 'ref2' => $this->ref2, 'ref3' => $this->ref3, 'ref4' => $this->ref4, 'ref5' => $this->ref5, 'userId' => $this->userId ); $this->getStore()->update($sql, $params); // Update the group // This is essentially a dirty edit (i.e. we don't touch the group assignments) $group = $this->userGroupFactory->getById($this->groupId); $group->group = $this->userName; $group->isSystemNotification = $this->isSystemNotification; $group->isDisplayNotification = $this->isDisplayNotification; $group->isCustomNotification = $this->isCustomNotification; $group->isDataSetNotification = $this->isDataSetNotification; $group->isLayoutNotification = $this->isLayoutNotification; $group->isLibraryNotification = $this->isLibraryNotification; $group->isReportNotification = $this->isReportNotification; $group->isScheduleNotification = $this->isScheduleNotification; // Do not update libraryQuota unless explicitly provided. // This preserves the current value instead of resetting it to null or 0. if (!empty($this->libraryQuota)) { $group->libraryQuota = $this->libraryQuota; } $group->save(['linkUsers' => false]); } /** * Update user */ private function updatePassword() { $this->getLog()->debug('Update user password. %d', $this->userId); $sql = 'UPDATE `user` SET CSPRNG = :CSPRNG, `UserPassword` = :password WHERE userId = :userId'; $params = array( 'CSPRNG' => $this->CSPRNG, 'password' => $this->password, 'userId' => $this->userId ); $this->getStore()->update($sql, $params); } /** * Update the Last Accessed date * @param bool $forcePasswordChange */ public function touch($forcePasswordChange = false) { $sql = 'UPDATE `user` SET lastAccessed = :time '; if ($forcePasswordChange) { $sql .= ' , isPasswordChangeRequired = 1 '; } $sql .= ' WHERE userId = :userId'; // This needs to happen on a separate connection $this->getStore()->update($sql, [ 'userId' => $this->userId, 'time' => date("Y-m-d H:i:s") ]); } /** * Get all features allowed for this user, including ones from their group * @return array */ public function getFeatures() { if ($this->resolvedFeatures === null) { $this->resolvedFeatures = $this->userGroupFactory->getGroupFeaturesForUser($this); } return $this->resolvedFeatures; } /** * Check whether the requested feature is available. * @param string|array $feature * @param bool $bothRequired * @return bool */ public function featureEnabled($feature, $bothRequired = false) { if ($this->isSuperAdmin()) { return true; } if (!is_array($feature)) { $feature = [$feature]; } if ($bothRequired) { return count($feature) === $this->featureEnabledCount($feature); } foreach ($feature as $item) { if (in_array($item, $this->getFeatures())) { return true; } } return false; } /** * Given an array of features, count the ones that are enabled * @param array $routes * @return int */ public function featureEnabledCount(array $routes) { // Shortcut for super admins. if ($this->isSuperAdmin()) { return count($routes); } // Test each route $count = 0; foreach ($routes as $route) { if ($this->featureEnabled($route)) { $count++; } } return $count; } /** * Is a particular type of notification enabled? * used by the user edit form * @param string $type The type of notification * @param bool $isGroupOnly If true, only return if a user group has the notification type enabled * @return bool * @throws \Xibo\Support\Exception\NotFoundException */ public function isNotificationEnabled(string $type, bool $isGroupOnly = false): bool { $this->load(); switch ($type) { case 'system': foreach ($this->groups as $group) { if ($group->getOriginalValue('isSystemNotification') == 1) { return true; } } if ($isGroupOnly) { return false; } else { return $this->isSystemNotification; } case 'display': foreach ($this->groups as $group) { if ($group->getOriginalValue('isDisplayNotification') == 1) { return true; } } if ($isGroupOnly) { return false; } else { return $this->isDisplayNotification; } case 'dataset': foreach ($this->groups as $group) { if ($group->getOriginalValue('isDataSetNotification') == 1) { return true; } } if ($isGroupOnly) { return false; } else { return $this->isDataSetNotification; } case 'layout': foreach ($this->groups as $group) { if ($group->getOriginalValue('isLayoutNotification') == 1) { return true; } } if ($isGroupOnly) { return false; } else { return $this->isLayoutNotification; } case 'library': foreach ($this->groups as $group) { if ($group->getOriginalValue('isLibraryNotification') == 1) { return true; } } if ($isGroupOnly) { return false; } else { return $this->isLibraryNotification; } case 'report': foreach ($this->groups as $group) { if ($group->getOriginalValue('isReportNotification') == 1) { return true; } } if ($isGroupOnly) { return false; } else { return $this->isReportNotification; } case 'schedule': foreach ($this->groups as $group) { if ($group->getOriginalValue('isScheduleNotification') == 1) { return true; } } if ($isGroupOnly) { return false; } else { return $this->isScheduleNotification; } case 'custom': foreach ($this->groups as $group) { if ($group->getOriginalValue('isCustomNotification') == 1) { return true; } } if ($isGroupOnly) { return false; } else { return $this->isCustomNotification; } default: return false; } } /** * Load permissions for a particular entity * @param string $entity * @return \Xibo\Entity\Permission[] */ private function loadPermissions(string $entity) { // Check our cache to see if we have permissions for this entity cached already if (!isset($this->permissionCache[$entity])) { // Store the results in the cache (default to empty result) $this->permissionCache[$entity] = array(); // Turn it into an ID keyed array foreach ($this->permissionFactory->getByUserId($entity, $this->userId) as $permission) { // Always take the max if (array_key_exists($permission->objectId, $this->permissionCache[$entity])) { $old = $this->permissionCache[$entity][$permission->objectId]; // Create a new permission record with the max of current and new $new = $this->permissionFactory->createEmpty(); $new->view = max($permission->view, $old->view); $new->edit = max($permission->edit, $old->edit); $new->delete = max($permission->delete, $old->delete); $this->permissionCache[$entity][$permission->objectId] = $new; } else { $this->permissionCache[$entity][$permission->objectId] = $permission; } } } return $this->permissionCache[$entity]; } /** * Check that this object can be used with the permissions sytem * @param object $object * @throws InvalidArgumentException */ private function checkObjectCompatibility($object): void { if (!method_exists($object, 'getId') || !method_exists($object, 'getOwnerId') || !method_exists($object, 'permissionsClass') ) { throw new InvalidArgumentException(__('Provided Object not under permission management'), 'object'); } } /** * Get a permission object * @param object $object * @return \Xibo\Entity\Permission * @throws InvalidArgumentException */ public function getPermission($object) { // Check that this object has the necessary methods $this->checkObjectCompatibility($object); // Admin users if ($this->isSuperAdmin() || $this->userId == $object->getOwnerId()) { return $this->permissionFactory->getFullPermissions(); } // Group Admins if ($this->userTypeId == 2 && count(array_intersect($this->groups, $this->userGroupFactory->getByUserId($object->getOwnerId())))) // Group Admin and in the same group as the owner. return $this->permissionFactory->getFullPermissions(); // Get the permissions for that entity $permissions = $this->loadPermissions($object->permissionsClass()); // Check to see if our object is in the list if (array_key_exists($object->getId(), $permissions)) return $permissions[$object->getId()]; else return $this->permissionFactory->createEmpty(); } /** * Check the given object is viewable * @param object $object * @return bool * @throws InvalidArgumentException */ public function checkViewable($object) { // Check that this object has the necessary methods $this->checkObjectCompatibility($object); // Admin users if ($this->isSuperAdmin() || $this->userId == $object->getOwnerId()) return true; // Group Admins if ($this->userTypeId == 2 && count(array_intersect($this->groups, $this->userGroupFactory->getByUserId($object->getOwnerId())))) // Group Admin and in the same group as the owner. return true; // Get the permissions for that entity $permissions = $this->loadPermissions($object->permissionsClass()); $folderPermissions = $this->loadPermissions('Xibo\Entity\Folder'); // If we are checking for view permissions on a folder, then we always grant those to a users home folder. if ($object->permissionsClass() === 'Xibo\Entity\Folder' && $object->getId() === $this->homeFolderId ) { return true; } // Check to see if our object is in the list if (array_key_exists($object->getId(), $permissions)) { return ($permissions[$object->getId()]->view == 1); } else if (method_exists($object, 'getPermissionFolderId') && array_key_exists($object->getPermissionFolderId(), $folderPermissions)) { return ($folderPermissions[$object->getPermissionFolderId()]->view == 1); } else { return false; } } /** * Check the given object is editable * @param object $object * @return bool * @throws InvalidArgumentException */ public function checkEditable($object) { // Check that this object has the necessary methods $this->checkObjectCompatibility($object); // Admin users if ($this->isSuperAdmin() || $this->userId == $object->getOwnerId()) { return true; } // Group Admins if ($object->permissionsClass() === 'Xibo\Entity\UserGroup') { // userGroup does not have an owner (getOwnerId() returns 0 ), we need to handle it in a different way. if ($this->userTypeId == 2 && count(array_intersect($this->groups, [$object]))) { // Group Admin and group object in the user array of groups return true; } } else { if ($this->userTypeId == 2 && count(array_intersect($this->groups, $this->userGroupFactory->getByUserId($object->getOwnerId())))) { // Group Admin and in the same group as the owner. return true; } } // Get the permissions for that entity $permissions = $this->loadPermissions($object->permissionsClass()); $folderPermissions = $this->loadPermissions('Xibo\Entity\Folder'); // Check to see if our object is in the list if (array_key_exists($object->getId(), $permissions)) { return ($permissions[$object->getId()]->edit == 1); } else if (method_exists($object, 'getPermissionFolderId') && array_key_exists($object->getPermissionFolderId(), $folderPermissions)) { return ($folderPermissions[$object->getPermissionFolderId()]->edit == 1); } else { return false; } } /** * Check the given object is delete-able * @param object $object * @return bool * @throws InvalidArgumentException */ public function checkDeleteable($object) { // Check that this object has the necessary methods $this->checkObjectCompatibility($object); // Admin users if ($this->userTypeId == 1 || $this->userId == $object->getOwnerId()) { return true; } // Group Admins if ($this->userTypeId == 2 && count(array_intersect($this->groups, $this->userGroupFactory->getByUserId($object->getOwnerId())))) { // Group Admin and in the same group as the owner. return true; } // Get the permissions for that entity $permissions = $this->loadPermissions($object->permissionsClass()); $folderPermissions = $this->loadPermissions('Xibo\Entity\Folder'); // Check to see if our object is in the list if (array_key_exists($object->getId(), $permissions)) { return ($permissions[$object->getId()]->delete == 1); } else if (method_exists($object, 'getPermissionFolderId') && array_key_exists($object->getPermissionFolderId(), $folderPermissions)) { return ($folderPermissions[$object->getPermissionFolderId()]->delete == 1); } else { return false; } } /** * Check the given objects permissions are modify-able * @param object $object * @return bool * @throws InvalidArgumentException */ public function checkPermissionsModifyable($object) { // Check that this object has the necessary methods $this->checkObjectCompatibility($object); // Admin users if ($this->userTypeId == 1 || ($this->userId == $object->getOwnerId() && $this->featureEnabled('user.sharing'))) return true; // Group Admins else if ($this->userTypeId == 2 && count(array_intersect($this->groups, $this->userGroupFactory->getByUserId($object->getOwnerId()))) && $this->featureEnabled('user.sharing')) // Group Admin and in the same group as the owner. return true; else return false; } /** * Returns the usertypeid for this user object. * @return int */ public function getUserTypeId() { return $this->userTypeId; } /** * Is a super admin * @return bool */ public function isSuperAdmin() { return ($this->userTypeId == 1); } /** * Is Group Admin * @return bool */ public function isGroupAdmin() { return ($this->userTypeId == 2); } /** * Is this users library quota full * @param boolean $reconnect * @throws LibraryFullException when the library is full or cannot be determined */ public function isQuotaFullByUser($reconnect = false) { $groupId = 0; $userQuota = 0; // Get the maximum quota of this users groups and their own quota $rows = $this->getStore()->select(' SELECT group.groupId, IFNULL(group.libraryQuota, 0) AS libraryQuota FROM `group` INNER JOIN `lkusergroup` ON group.groupId = lkusergroup.groupId WHERE lkusergroup.userId = :userId ORDER BY `group`.isUserSpecific DESC, IFNULL(group.libraryQuota, 0) DESC ', ['userId' => $this->userId], 'default', $reconnect); if (count($rows) <= 0) { throw new LibraryFullException('Problem calculating this users library quota.'); } foreach ($rows as $row) { if ($row['libraryQuota'] > 0) { $groupId = $row['groupId']; $userQuota = intval($row['libraryQuota']); break; } } if ($userQuota > 0) { // If there is a quota, then test it against the current library position for this user. // use the groupId that generated the quota in order to calculate the usage $rows = $this->getStore()->select(' SELECT IFNULL(SUM(FileSize), 0) AS SumSize FROM `media` INNER JOIN `lkusergroup` ON lkusergroup.userId = media.userId WHERE lkusergroup.groupId = :groupId ', ['groupId' => $groupId], 'default', $reconnect); if (count($rows) <= 0) { throw new LibraryFullException("Error Processing Request", 1); } $fileSize = intval($rows[0]['SumSize']); if (($fileSize / 1024) >= $userQuota) { $this->getLog()->debug('User has exceeded library quota. FileSize: ' . $fileSize . ' bytes, quota is ' . $userQuota * 1024); throw new LibraryFullException(__('You have exceeded your library quota')); } } } /** * Tests the supplied password against the password policy * @param string $password * @throws InvalidArgumentException */ public function testPasswordAgainstPolicy($password) { // Check password complexity $policy = $this->configService->getSetting('USER_PASSWORD_POLICY'); if ($policy != '') { $policyError = $this->configService->getSetting('USER_PASSWORD_ERROR'); $policyError = ($policyError == '') ? __('Your password does not meet the required complexity') : $policyError; if(!preg_match($policy, $password, $matches)) { throw new InvalidArgumentException($policyError); } } } /** * @return UserOption[] */ public function getUserOptions() { // Don't return anything with Grid in it (these have to be specifically requested). return array_values(array_filter($this->userOptions, function($element) { return !(stripos($element->option, 'Grid')); })); } /** * Clear the two factor stored secret and recovery codes */ public function clearTwoFactor() { $this->twoFactorTypeId = 0; $this->twoFactorSecret = NULL; $this->twoFactorRecoveryCodes = NULL; $sql = 'UPDATE `user` SET twoFactorSecret = :twoFactorSecret, twoFactorTypeId = :twoFactorTypeId, twoFactorRecoveryCodes =:twoFactorRecoveryCodes WHERE userId = :userId'; $params = [ 'userId' => $this->userId, 'twoFactorSecret' => $this->twoFactorSecret, 'twoFactorTypeId' => $this->twoFactorTypeId, 'twoFactorRecoveryCodes' => $this->twoFactorRecoveryCodes ]; $this->getStore()->update($sql, $params); } /** * @param $recoveryCodes */ public function updateRecoveryCodes($recoveryCodes) { $sql = 'UPDATE `user` SET twoFactorRecoveryCodes = :twoFactorRecoveryCodes WHERE userId = :userId'; $params = [ 'userId' => $this->userId, 'twoFactorRecoveryCodes' => $recoveryCodes ]; $this->getStore()->update($sql, $params); } }