mirror of
https://github.com/nextcloud/server.git
synced 2026-04-21 06:08:46 -04:00
Fix psalm issues related to the user backend
- Reflect the actual return value returned by the implementation in the the interface. E.g. IUser|bool -> IUser|false - Remove $hasLoggedIn parameter from private countUser implementation. Replace the two call with the equivalent countSeenUser - getBackend is nuallable, add this to the interface - Use backend interface to make psalm happy about call to undefined methods. Also helps with getting rid at some point of the old implementActions Signed-off-by: Carl Schwan <carl@carlschwan.eu>
This commit is contained in:
parent
eb45a6ca40
commit
9ec0cb0a90
13 changed files with 59 additions and 101 deletions
|
|
@ -2310,11 +2310,6 @@
|
|||
<code>searchCollections</code>
|
||||
</UndefinedInterfaceMethod>
|
||||
</file>
|
||||
<file src="core/Controller/SvgController.php">
|
||||
<TypeDoesNotContainNull occurrences="1">
|
||||
<code>$svg === null</code>
|
||||
</TypeDoesNotContainNull>
|
||||
</file>
|
||||
<file src="core/Controller/UnifiedSearchController.php">
|
||||
<NullArgument occurrences="1">
|
||||
<code>null</code>
|
||||
|
|
@ -4354,28 +4349,11 @@
|
|||
<FalsableReturnStatement occurrences="1">
|
||||
<code>false</code>
|
||||
</FalsableReturnStatement>
|
||||
<ImplicitToStringCast occurrences="1">
|
||||
<code>$query->func()->lower('displayname')</code>
|
||||
</ImplicitToStringCast>
|
||||
</file>
|
||||
<file src="lib/private/User/Manager.php">
|
||||
<ImplementedReturnTypeMismatch occurrences="1">
|
||||
<code>array|int</code>
|
||||
</ImplementedReturnTypeMismatch>
|
||||
<InvalidArgument occurrences="1">
|
||||
<code>$callback</code>
|
||||
</InvalidArgument>
|
||||
<InvalidNullableReturnType occurrences="1">
|
||||
<code>bool|IUser</code>
|
||||
</InvalidNullableReturnType>
|
||||
<NullableReturnStatement occurrences="2">
|
||||
<code>$this->createUserFromBackend($uid, $password, $backend)</code>
|
||||
<code>$this->createUserFromBackend($uid, $password, $backend)</code>
|
||||
</NullableReturnStatement>
|
||||
<UndefinedInterfaceMethod occurrences="5">
|
||||
<code>checkPassword</code>
|
||||
<code>checkPassword</code>
|
||||
<code>countUsers</code>
|
||||
<ImplementedReturnTypeMismatch occurrences="1"/>
|
||||
<InvalidArgument occurrences="1"/>
|
||||
<UndefinedInterfaceMethod occurrences="2">
|
||||
<code>createUser</code>
|
||||
<code>getUsersForUserValueCaseInsensitive</code>
|
||||
</UndefinedInterfaceMethod>
|
||||
|
|
@ -4387,23 +4365,13 @@
|
|||
<InvalidArgument occurrences="1">
|
||||
<code>IUser::class . '::firstLogin'</code>
|
||||
</InvalidArgument>
|
||||
<InvalidScalarArgument occurrences="2">
|
||||
<code>$this->timeFactory->getTime()</code>
|
||||
<code>$this->timeFactory->getTime()</code>
|
||||
</InvalidScalarArgument>
|
||||
<NoInterfaceProperties occurrences="2">
|
||||
<code>$request->server</code>
|
||||
<code>$request->server</code>
|
||||
</NoInterfaceProperties>
|
||||
<NullableReturnStatement occurrences="1">
|
||||
<code>null</code>
|
||||
</NullableReturnStatement>
|
||||
<TooManyArguments occurrences="1">
|
||||
<code>dispatch</code>
|
||||
</TooManyArguments>
|
||||
<UndefinedMethod occurrences="1">
|
||||
<code>getByEmail</code>
|
||||
</UndefinedMethod>
|
||||
</file>
|
||||
<file src="lib/private/User/User.php">
|
||||
<InvalidArgument occurrences="5">
|
||||
|
|
@ -4413,22 +4381,6 @@
|
|||
<code>IUser::class . '::preDelete'</code>
|
||||
<code>IUser::class . '::preSetPassword'</code>
|
||||
</InvalidArgument>
|
||||
<InvalidNullableReturnType occurrences="1">
|
||||
<code>getBackend</code>
|
||||
</InvalidNullableReturnType>
|
||||
<InvalidReturnStatement occurrences="1">
|
||||
<code>$image</code>
|
||||
</InvalidReturnStatement>
|
||||
<InvalidReturnType occurrences="1">
|
||||
<code>IImage|null</code>
|
||||
</InvalidReturnType>
|
||||
<InvalidScalarArgument occurrences="2">
|
||||
<code>$quota</code>
|
||||
<code>$this->lastLogin</code>
|
||||
</InvalidScalarArgument>
|
||||
<NullableReturnStatement occurrences="1">
|
||||
<code>$this->backend</code>
|
||||
</NullableReturnStatement>
|
||||
<TooManyArguments occurrences="5">
|
||||
<code>dispatch</code>
|
||||
<code>dispatch</code>
|
||||
|
|
@ -4436,13 +4388,6 @@
|
|||
<code>dispatch</code>
|
||||
<code>dispatch</code>
|
||||
</TooManyArguments>
|
||||
<UndefinedInterfaceMethod occurrences="5">
|
||||
<code>canChangeAvatar</code>
|
||||
<code>deleteUserAvatar</code>
|
||||
<code>getHome</code>
|
||||
<code>setDisplayName</code>
|
||||
<code>setPassword</code>
|
||||
</UndefinedInterfaceMethod>
|
||||
</file>
|
||||
<file src="lib/private/legacy/OC_API.php">
|
||||
<InvalidNullableReturnType occurrences="1">
|
||||
|
|
|
|||
|
|
@ -1096,7 +1096,7 @@ class QueryBuilder implements IQueryBuilder {
|
|||
* Specifies an ordering for the query results.
|
||||
* Replaces any previously specified orderings, if any.
|
||||
*
|
||||
* @param string $sort The ordering expression.
|
||||
* @param string|IQueryFunction|ILiteral|IParameter $sort The ordering expression.
|
||||
* @param string $order The ordering direction.
|
||||
*
|
||||
* @return $this This QueryBuilder instance.
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ class Database extends ABackend implements
|
|||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
$result = $query->execute();
|
||||
$result = $query->executeQuery();
|
||||
$displayNames = [];
|
||||
while ($row = $result->fetch()) {
|
||||
$displayNames[(string)$row['uid']] = (string)$row['displayname'];
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ namespace OC\User;
|
|||
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\UserInterface;
|
||||
|
||||
class LazyUser implements IUser {
|
||||
private ?IUser $user = null;
|
||||
|
|
@ -83,7 +84,7 @@ class LazyUser implements IUser {
|
|||
return $this->getUser()->getBackendClassName();
|
||||
}
|
||||
|
||||
public function getBackend() {
|
||||
public function getBackend(): ?UserInterface {
|
||||
return $this->getUser()->getBackend();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ use OCP\Notification\IManager;
|
|||
use OCP\Support\Subscription\IRegistry;
|
||||
use OCP\User\Backend\IGetRealUIDBackend;
|
||||
use OCP\User\Backend\ISearchKnownUsersBackend;
|
||||
use OCP\User\Backend\ICheckPasswordBackend;
|
||||
use OCP\User\Backend\ICountUsersBackend;
|
||||
use OCP\User\Events\BeforeUserCreatedEvent;
|
||||
use OCP\User\Events\UserCreatedEvent;
|
||||
use OCP\UserInterface;
|
||||
|
|
@ -223,7 +225,7 @@ class Manager extends PublicEmitter implements IUserManager {
|
|||
*
|
||||
* @param string $loginName
|
||||
* @param string $password
|
||||
* @return mixed the User object on success, false otherwise
|
||||
* @return IUser|false the User object on success, false otherwise
|
||||
*/
|
||||
public function checkPassword($loginName, $password) {
|
||||
$result = $this->checkPasswordNoLogging($loginName, $password);
|
||||
|
|
@ -254,7 +256,8 @@ class Manager extends PublicEmitter implements IUserManager {
|
|||
$backends = $this->backends;
|
||||
}
|
||||
foreach ($backends as $backend) {
|
||||
if ($backend->implementsActions(Backend::CHECK_PASSWORD)) {
|
||||
if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
|
||||
/** @var ICheckPasswordBackend $backend */
|
||||
$uid = $backend->checkPassword($loginName, $password);
|
||||
if ($uid !== false) {
|
||||
return $this->getUserObject($uid, $backend);
|
||||
|
|
@ -268,7 +271,8 @@ class Manager extends PublicEmitter implements IUserManager {
|
|||
$password = urldecode($password);
|
||||
|
||||
foreach ($backends as $backend) {
|
||||
if ($backend->implementsActions(Backend::CHECK_PASSWORD)) {
|
||||
if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
|
||||
/** @var ICheckPasswordBackend|UserInterface $backend */
|
||||
$uid = $backend->checkPassword($loginName, $password);
|
||||
if ($uid !== false) {
|
||||
return $this->getUserObject($uid, $backend);
|
||||
|
|
@ -376,7 +380,7 @@ class Manager extends PublicEmitter implements IUserManager {
|
|||
* @param string $uid
|
||||
* @param string $password
|
||||
* @throws \InvalidArgumentException
|
||||
* @return bool|IUser the created user or false
|
||||
* @return false|IUser the created user or false
|
||||
*/
|
||||
public function createUser($uid, $password) {
|
||||
// DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency
|
||||
|
|
@ -415,7 +419,7 @@ class Manager extends PublicEmitter implements IUserManager {
|
|||
* @param string $uid
|
||||
* @param string $password
|
||||
* @param UserInterface $backend
|
||||
* @return IUser|null
|
||||
* @return IUser|false
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function createUserFromBackend($uid, $password, UserInterface $backend) {
|
||||
|
|
@ -469,8 +473,9 @@ class Manager extends PublicEmitter implements IUserManager {
|
|||
/** @deprecated 21.0.0 use UserCreatedEvent event with the IEventDispatcher instead */
|
||||
$this->emit('\OC\User', 'postCreateUser', [$user, $password]);
|
||||
$this->eventDispatcher->dispatchTyped(new UserCreatedEvent($user, $password));
|
||||
return $user;
|
||||
}
|
||||
return $user;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -478,16 +483,13 @@ class Manager extends PublicEmitter implements IUserManager {
|
|||
*
|
||||
* @param boolean $hasLoggedIn when true only users that have a lastLogin
|
||||
* entry in the preferences table will be affected
|
||||
* @return array|int an array of backend class as key and count number as value
|
||||
* if $hasLoggedIn is true only an int is returned
|
||||
* @return array<string, int> an array of backend class as key and count number as value
|
||||
*/
|
||||
public function countUsers($hasLoggedIn = false) {
|
||||
if ($hasLoggedIn) {
|
||||
return $this->countSeenUsers();
|
||||
}
|
||||
public function countUsers() {
|
||||
$userCountStatistics = [];
|
||||
foreach ($this->backends as $backend) {
|
||||
if ($backend->implementsActions(Backend::COUNT_USERS)) {
|
||||
if ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
|
||||
/** @var ICountUsersBackend|IUserBackend $backend */
|
||||
$backendUsers = $backend->countUsers();
|
||||
if ($backendUsers !== false) {
|
||||
if ($backend instanceof IUserBackend) {
|
||||
|
|
@ -528,7 +530,7 @@ class Manager extends PublicEmitter implements IUserManager {
|
|||
* The callback is executed for each user on each backend.
|
||||
* If the callback returns false no further users will be retrieved.
|
||||
*
|
||||
* @param \Closure $callback
|
||||
* @psalm-param \Closure(\OCP\IUser):?bool $callback
|
||||
* @param string $search
|
||||
* @param boolean $onlySeen when true only users that have a lastLogin entry
|
||||
* in the preferences table will be affected
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ use Symfony\Component\EventDispatcher\GenericEvent;
|
|||
*/
|
||||
class Session implements IUserSession, Emitter {
|
||||
|
||||
/** @var Manager|PublicEmitter $manager */
|
||||
/** @var Manager $manager */
|
||||
private $manager;
|
||||
|
||||
/** @var ISession $session */
|
||||
|
|
@ -288,9 +288,9 @@ class Session implements IUserSession, Emitter {
|
|||
}
|
||||
|
||||
/**
|
||||
* get the login name of the current user
|
||||
* Get the login name of the current user
|
||||
*
|
||||
* @return string
|
||||
* @return ?string
|
||||
*/
|
||||
public function getLoginName() {
|
||||
if ($this->activeUser) {
|
||||
|
|
@ -870,7 +870,7 @@ class Session implements IUserSession, Emitter {
|
|||
// replace successfully used token with a new one
|
||||
$this->config->deleteUserValue($uid, 'login_token', $currentToken);
|
||||
$newToken = $this->random->generate(32);
|
||||
$this->config->setUserValue($uid, 'login_token', $newToken, $this->timeFactory->getTime());
|
||||
$this->config->setUserValue($uid, 'login_token', $newToken, (string)$this->timeFactory->getTime());
|
||||
|
||||
try {
|
||||
$sessionId = $this->session->getId();
|
||||
|
|
@ -905,7 +905,7 @@ class Session implements IUserSession, Emitter {
|
|||
*/
|
||||
public function createRememberMeToken(IUser $user) {
|
||||
$token = $this->random->generate(32);
|
||||
$this->config->setUserValue($user->getUID(), 'login_token', $token, $this->timeFactory->getTime());
|
||||
$this->config->setUserValue($user->getUID(), 'login_token', $token, (string)$this->timeFactory->getTime());
|
||||
$this->setMagicInCookie($user->getUID(), $token);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ use OCP\IUserBackend;
|
|||
use OCP\User\Events\BeforeUserDeletedEvent;
|
||||
use OCP\User\Events\UserDeletedEvent;
|
||||
use OCP\User\GetQuotaEvent;
|
||||
use OCP\User\Backend\ISetDisplayNameBackend;
|
||||
use OCP\User\Backend\ISetPasswordBackend;
|
||||
use OCP\User\Backend\IProvideAvatarBackend;
|
||||
use OCP\User\Backend\IGetHomeBackend;
|
||||
use OCP\UserInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\GenericEvent;
|
||||
|
|
@ -155,7 +159,9 @@ class User implements IUser {
|
|||
$displayName = trim($displayName);
|
||||
$oldDisplayName = $this->getDisplayName();
|
||||
if ($this->backend->implementsActions(Backend::SET_DISPLAYNAME) && !empty($displayName) && $displayName !== $oldDisplayName) {
|
||||
$result = $this->backend->setDisplayName($this->uid, $displayName);
|
||||
/** @var ISetDisplayNameBackend $backend */
|
||||
$backend = $this->backend;
|
||||
$result = $backend->setDisplayName($this->uid, $displayName);
|
||||
if ($result) {
|
||||
$this->displayName = $displayName;
|
||||
$this->triggerChange('displayName', $displayName, $oldDisplayName);
|
||||
|
|
@ -241,7 +247,7 @@ class User implements IUser {
|
|||
$firstTimeLogin = ($this->getLastLogin() === 0);
|
||||
$this->lastLogin = time();
|
||||
$this->config->setUserValue(
|
||||
$this->uid, 'login', 'lastLogin', $this->lastLogin);
|
||||
$this->uid, 'login', 'lastLogin', (string)$this->lastLogin);
|
||||
|
||||
return $firstTimeLogin;
|
||||
}
|
||||
|
|
@ -280,7 +286,7 @@ class User implements IUser {
|
|||
\OC::$server->getCommentsManager()->deleteReferencesOfActor('users', $this->uid);
|
||||
\OC::$server->getCommentsManager()->deleteReadMarksFromUser($this);
|
||||
|
||||
/** @var IAvatarManager $avatarManager */
|
||||
/** @var AvatarManager $avatarManager */
|
||||
$avatarManager = \OC::$server->query(AvatarManager::class);
|
||||
$avatarManager->deleteUserAvatar($this->uid);
|
||||
|
||||
|
|
@ -319,7 +325,9 @@ class User implements IUser {
|
|||
$this->emitter->emit('\OC\User', 'preSetPassword', [$this, $password, $recoveryPassword]);
|
||||
}
|
||||
if ($this->backend->implementsActions(Backend::SET_PASSWORD)) {
|
||||
$result = $this->backend->setPassword($this->uid, $password);
|
||||
/** @var ISetPasswordBackend $backend */
|
||||
$backend = $this->backend;
|
||||
$result = $backend->setPassword($this->uid, $password);
|
||||
|
||||
if ($result !== false) {
|
||||
$this->legacyDispatcher->dispatch(IUser::class . '::postSetPassword', new GenericEvent($this, [
|
||||
|
|
@ -344,7 +352,8 @@ class User implements IUser {
|
|||
*/
|
||||
public function getHome() {
|
||||
if (!$this->home) {
|
||||
if ($this->backend->implementsActions(Backend::GET_HOME) and $home = $this->backend->getHome($this->uid)) {
|
||||
/** @psalm-suppress UndefinedInterfaceMethod Once we get rid of the legacy implementsActions, psalm won't complain anymore */
|
||||
if (($this->backend instanceof IGetHomeBackend || $this->backend->implementsActions(Backend::GET_HOME)) && $home = $this->backend->getHome($this->uid)) {
|
||||
$this->home = $home;
|
||||
} elseif ($this->config) {
|
||||
$this->home = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $this->uid;
|
||||
|
|
@ -367,18 +376,20 @@ class User implements IUser {
|
|||
return get_class($this->backend);
|
||||
}
|
||||
|
||||
public function getBackend() {
|
||||
public function getBackend(): ?UserInterface {
|
||||
return $this->backend;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if the backend allows the user to change his avatar on Personal page
|
||||
* Check if the backend allows the user to change his avatar on Personal page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canChangeAvatar() {
|
||||
if ($this->backend->implementsActions(Backend::PROVIDE_AVATAR)) {
|
||||
return $this->backend->canChangeAvatar($this->uid);
|
||||
if ($this->backend instanceof IProvideAvatarBackend || $this->backend->implementsActions(Backend::PROVIDE_AVATAR)) {
|
||||
/** @var IProvideAvatarBackend $backend */
|
||||
$backend = $this->backend;
|
||||
return $backend->canChangeAvatar($this->uid);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -501,7 +512,7 @@ class User implements IUser {
|
|||
$oldQuota = $this->config->getUserValue($this->uid, 'files', 'quota', '');
|
||||
if ($quota !== 'none' and $quota !== 'default') {
|
||||
$quota = OC_Helper::computerFileSize($quota);
|
||||
$quota = OC_Helper::humanFileSize($quota);
|
||||
$quota = OC_Helper::humanFileSize((int)$quota);
|
||||
}
|
||||
if ($quota !== $oldQuota) {
|
||||
$this->config->setUserValue($this->uid, 'files', 'quota', $quota);
|
||||
|
|
|
|||
|
|
@ -820,7 +820,7 @@ interface IQueryBuilder {
|
|||
* Specifies an ordering for the query results.
|
||||
* Replaces any previously specified orderings, if any.
|
||||
*
|
||||
* @param string $sort The ordering expression.
|
||||
* @param string|IQueryFunction $sort The ordering expression.
|
||||
* @param string $order The ordering direction.
|
||||
*
|
||||
* @return $this This QueryBuilder instance.
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ interface IAvatar {
|
|||
/**
|
||||
* get the users avatar
|
||||
* @param int $size size in px of the avatar, avatars are square, defaults to 64, -1 can be used to not scale the image
|
||||
* @return boolean|\OCP\IImage containing the avatar or false if there's no image
|
||||
* @return false|\OCP\IImage containing the avatar or false if there's no image
|
||||
* @since 6.0.0 - size of -1 was added in 9.0.0
|
||||
*/
|
||||
public function get($size = 64);
|
||||
|
|
|
|||
|
|
@ -113,10 +113,9 @@ interface IUser {
|
|||
/**
|
||||
* Get the backend for the current user object
|
||||
*
|
||||
* @return UserInterface
|
||||
* @since 15.0.0
|
||||
*/
|
||||
public function getBackend();
|
||||
public function getBackend(): ?UserInterface;
|
||||
|
||||
/**
|
||||
* check if the backend allows the user to change his avatar on Personal page
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ interface IUserManager {
|
|||
*
|
||||
* @param string $loginName
|
||||
* @param string $password
|
||||
* @return mixed the User object on success, false otherwise
|
||||
* @return IUser|false the User object on success, false otherwise
|
||||
* @since 8.0.0
|
||||
*/
|
||||
public function checkPassword($loginName, $password);
|
||||
|
|
@ -141,7 +141,7 @@ interface IUserManager {
|
|||
* @param string $uid
|
||||
* @param string $password
|
||||
* @throws \InvalidArgumentException
|
||||
* @return bool|\OCP\IUser the created user or false
|
||||
* @return false|\OCP\IUser the created user or false
|
||||
* @since 8.0.0
|
||||
*/
|
||||
public function createUser($uid, $password);
|
||||
|
|
@ -157,9 +157,9 @@ interface IUserManager {
|
|||
public function createUserFromBackend($uid, $password, UserInterface $backend);
|
||||
|
||||
/**
|
||||
* returns how many users per backend exist (if supported by backend)
|
||||
* Get how many users per backend exist (if supported by backend)
|
||||
*
|
||||
* @return array an array of backend class as key and count number as value
|
||||
* @return array<string, int> an array of backend class name as key and count number as value
|
||||
* @since 8.0.0
|
||||
*/
|
||||
public function countUsers();
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ interface ICheckPasswordBackend {
|
|||
*
|
||||
* @param string $loginName The loginname
|
||||
* @param string $password The password
|
||||
* @return string|bool The uid on success false on failure
|
||||
* @return string|false The uid on success false on failure
|
||||
*/
|
||||
public function checkPassword(string $loginName, string $password);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -609,7 +609,7 @@ class ManagerTest extends TestCase {
|
|||
public function testCountUsersOnlySeen() {
|
||||
$manager = \OC::$server->getUserManager();
|
||||
// count other users in the db before adding our own
|
||||
$countBefore = $manager->countUsers(true);
|
||||
$countBefore = $manager->countSeenUsers();
|
||||
|
||||
//Add test users
|
||||
$user1 = $manager->createUser('testseencount1', 'testseencount1');
|
||||
|
|
@ -623,7 +623,7 @@ class ManagerTest extends TestCase {
|
|||
$user4 = $manager->createUser('testseencount4', 'testseencount4');
|
||||
$user4->updateLastLoginTimestamp();
|
||||
|
||||
$this->assertEquals($countBefore + 3, $manager->countUsers(true));
|
||||
$this->assertEquals($countBefore + 3, $manager->countSeenUsers());
|
||||
|
||||
//cleanup
|
||||
$user1->delete();
|
||||
|
|
|
|||
Loading…
Reference in a new issue