1578 lines
48 KiB
PHP
1578 lines
48 KiB
PHP
|
|
<?php
|
||
|
|
/*
|
||
|
|
* Copyright (C) 2025 Xibo Signage Ltd
|
||
|
|
*
|
||
|
|
* Xibo - Digital Signage - https://xibosignage.com
|
||
|
|
*
|
||
|
|
* This file is part of Xibo.
|
||
|
|
*
|
||
|
|
* Xibo is free software: you can redistribute it and/or modify
|
||
|
|
* it under the terms of the GNU Affero General Public License as published by
|
||
|
|
* the Free Software Foundation, either version 3 of the License, or
|
||
|
|
* any later version.
|
||
|
|
*
|
||
|
|
* Xibo is distributed in the hope that it will be useful,
|
||
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
* GNU Affero General Public License for more details.
|
||
|
|
*
|
||
|
|
* You should have received a copy of the GNU Affero General Public License
|
||
|
|
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
*/
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|