2017-12-06 81 views
6

Zaktualizowałem mój formularz środowiska Symfony od 3.3 do 4.0. Po aktualizacji mam problemy z logowaniem (użytkownik dostarczony przez bazę danych). Kiedy przesyłam formularz logowania, właśnie wróciłem do formularza logowania bez żadnego komunikatu o błędzie. Kiedy używam nieprawidłowych poświadczeń, otrzymuję odpowiedni komunikat o błędzie. Oto dziennik po próbie zalogowania. Logowanie użytkownika z dostawcą użytkownika "in_memory" działa. Czy potrzebujesz więcej informacji?Token został deauthenticated po próbie odświeżenia go

Dziękujemy!

[2017-12-06 13:57:05] security.INFO: User has been authenticated successfully. {"username":"***"} [] 
[2017-12-06 14:22:39] doctrine.DEBUG: "START TRANSACTION" [] [] 
[2017-12-06 13:57:05] security.DEBUG: Read existing security token from the session. {"key":"_security_secured_area","token_class":"Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken"} [] 
[2017-12-06 13:57:05] doctrine.DEBUG: SELECT t0.username AS username_1, t0.password AS password_2, t0.email AS email_3, t0.email_new AS email_new_4, t0.first_name AS first_name_5, t0.last_name AS last_name_6, t0.is_active AS is_active_7, t0.email_confirmed AS email_confirmed_8, t0.shibboleth_state AS shibboleth_state_9, t0.shibboleth_hash AS shibboleth_hash_10, t0.shibboleth_persistent_id AS shibboleth_persistent_id_11, t0.confirmation_email_send AS confirmation_email_send_12, t0.last_login AS last_login_13, t0.expires AS expires_14, t0.session_id AS session_id_15, t0.id AS id_16, t0.hidden AS hidden_17, t0.deleted AS deleted_18, t0.created AS created_19, t0.modified AS modified_20, t0.sorting AS sorting_21, t0.salutation_id AS salutation_id_22, t0.creator_id AS creator_id_23, t0.modifier_id AS modifier_id_24 FROM User t0 WHERE t0.id = ? AND ((t0.deleted = 0)) [2] [] 
[2017-12-06 13:57:05] security.DEBUG: Token was deauthenticated after trying to refresh it. {"username":"user","provider":"Symfony\\Component\\Security\\Core\\User\\ChainUserProvider"} [] 
[2017-12-06 13:57:05] security.INFO: Populated the TokenStorage with an anonymous Token. [] [] 
[2017-12-06 13:57:05] security.DEBUG: Access denied, the user is not fully authenticated; redirecting to authentication entry point. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException(code: 403): Access Denied. at /vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/AccessListener.php:68)"} [] 
[2017-12-06 13:57:05] security.DEBUG: Calling Authentication entry point. [] [] 

Entity \ User:

class User extends EntitySuperclass implements AdvancedUserInterface, \Serializable 
{ 
    /** 
    * @ORM\Column(type="string") 
    */ 
    private $username; 

    /** 
    * 
    * @Assert\Length(max=4096,groups={"account_complete","account_password","user"}) 
    * @Assert\Length(min = 8,groups={"account_complete","account_password","user"}, minMessage="user.password_length") 
    */ 
    private $plainPassword; 

    /** 
    * The below length depends on the "algorithm" you use for encoding 
    * the password, but this works well with bcrypt. 
    * 
    * @ORM\Column(type="string", length=64) 
    */ 
    private $password; 

    /** 
    * @ORM\Column(type="string", length=255) 
    * @Assert\NotBlank(groups={"account_register","user"}) 
    * @Assert\Email(
    *  groups = {"account_register", "account","user"}, 
    *  strict = true, 
    *  checkMX = true 
    *) 
    */ 
    private $email; 

    /** 
    * @ORM\Column(type="string", length=255) 
    */ 
    private $emailNew = ''; 

    /** 
    * @ORM\ManyToOne(targetEntity="Salutation") 
    * 
    */ 
    private $salutation; 

    /** 
    * @ORM\Column(type="string") 
    * @Assert\NotBlank(groups={"account_complete","user"}) 
    * @Assert\Regex(pattern = "/^[a-zA-ZäöüÄÖÜß0-9 ]+$/",groups={"account_complete","user"}, message="user.first_name.regex") 
    */ 
    private $firstName; 

    /** 
    * @ORM\Column(type="string") 
    * @Assert\NotBlank(groups={"account_complete","user"}) 
    * @Assert\Regex(pattern = "/^[a-zA-ZäöüÄÖÜß0-9 ]+$/",groups={"account_complete","user"}, message="user.last_name.regex") 
    */ 
    private $lastName; 

    /** 
    * @ORM\Column(name="is_active", type="boolean") 
    */ 
    private $isActive = false; 

    /** 
    * @ORM\Column(name="email_confirmed", type="boolean") 
    */ 
    private $emailConfirmed = false; 

    /** 
    * @ORM\Column(type="integer") 
    */ 
    private $shibbolethState = 0; 

    /** 
    * @ORM\Column(type="string") 
    */ 
    private $shibbolethHash = ''; 

    /** 
    * @ORM\Column(type="string") 
    */ 
    private $shibbolethPersistentId = ''; 

    /** 
    * @ORM\ManyToMany(targetEntity="UserGroup") 
    * @ORM\JoinTable(name="User_UserGroup", 
    *  joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, 
    *  inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")} 
    *  ) 
    */ 
    private $userGroups; 

    /** 
    * @ORM\Column(type="integer") 
    */ 
    private $confirmationEmailSend; 

    /** 
    * @ORM\Column(type="integer") 
    */ 
    private $lastLogin = 0; 

    /** 
    * @ORM\Column(type="integer") 
    */ 
    protected $expires = 0; 

    /** 
    * @ORM\Column(type="string", length=255) 
    */ 
    private $sessionId = ''; 

    /** 
    * @ORM\ManyToMany(targetEntity="BankDetails", cascade={"persist"}) 
    * @ORM\JoinTable(name="User_BankDetails", 
    *  joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, 
    *  inverseJoinColumns={@ORM\JoinColumn(name="bank_details_id", referencedColumnName="id")} 
    *) 
    * @Assert\Valid 
    */ 
    private $bankDetails; 

    /** 
    * @ORM\ManyToMany(targetEntity="Address", cascade={"persist"}) 
    * @ORM\JoinTable(name="User_BillingAddress", 
    *  joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, 
    *  inverseJoinColumns={@ORM\JoinColumn(name="billing_address_id", referencedColumnName="id")} 
    *) 
    * @Assert\Count(
    *  min = 1, 
    *  minMessage = "user.billing_addresses.min", 
    *) 
    * @Assert\Valid 
    */ 
    private $billingAddresses; 

    public function __construct() 
    { 
     parent::__construct(); 
     $this->isActive = true; 
     $this->confirmationEmailSend = 0; 
     $this->userGroups = new ArrayCollection(); 
     $this->bankDetails = new ArrayCollection(); 
     $this->billingAddresses = new ArrayCollection(); 
     // may not be needed, see section on salt below 
     // $this->salt = md5(uniqid(null, true)); 
    } 

    /** 
    * @ORM\PrePersist 
    */ 
    public function prePersist() 
    { 
     $currentTimestamp = time(); 

     if($this->getConfirmationEmailSend() == NULL) 
      $this->setConfirmationEmailSend(0); 

    } 

    public function getUsername() 
    { 
     //return $this->username; 
     return $this->email; 
    } 

    public function getSalt() 
    { 
     // The bcrypt algorithm doesn't require a separate salt. 
     return null; 
    } 

    public function getPassword() 
    { 
     return $this->password; 
    } 

    public function getRoles() 
    { 
     $roles = array(); 
     $userGroups = $this->getUserGroups(); 
     if(!empty($userGroups)) { 
      foreach($userGroups as $userGroup) { 
       $role = $userGroup->getRole(); 
       $roles[] = 'ROLE_'.strtoupper($role); 
      } 
     } 
     return $roles; 
    } 

    public function isGranted($role) 
    { 
     return in_array($role, $this->getRoles()); 
    } 

    public function eraseCredentials() 
    { 
    } 

    public function isAccountNonExpired() 
    { 
     return true; 
    } 

    public function isAccountNonLocked() 
    { 
     return true; 
    } 

    public function isCredentialsNonExpired() 
    { 
     return true; 
    } 

    public function isEnabled() 
    { 
     return $this->isActive; 
    } 

    /** @see \Serializable::serialize() */ 
    public function serialize() 
    { 
     return serialize(array(
      $this->id, 
      $this->username, 
      $this->password, 
      $this->isActive, 
      // see section on salt below 
      // $this->salt, 
     )); 
    } 

    /** @see \Serializable::unserialize() */ 
    public function unserialize($serialized) 
    { 
     list (
      $this->id, 
      $this->username, 
      $this->password, 
      $this->isActive, 
      // see section on salt below 
      // $this->salt 
     ) = unserialize($serialized); 
    } 

    /** 
    * Set username 
    * 
    * @param string $username 
    * 
    * @return User 
    */ 
    public function setUsername($username) 
    { 
     $this->username = $username; 

     return $this; 
    } 

    public function getPlainPassword() 
    { 
     return $this->plainPassword; 
    } 

    public function setPlainPassword($password) 
    { 
     $this->plainPassword = $password; 
    } 

    /** 
    * Set password 
    * 
    * @param string $password 
    * 
    * @return User 
    */ 
    public function setPassword($password) 
    { 
     $this->password = $password; 

     return $this; 
    } 

    /** 
    * Set email 
    * 
    * @param string $email 
    * 
    * @return User 
    */ 
    public function setEmail($email) 
    { 
     $this->email = $email; 
     $this->setUsername($email); 

     return $this; 
    } 

    /** 
    * Get email 
    * 
    * @return string 
    */ 
    public function getEmail() 
    { 
     return $this->email; 
    } 

    /** 
    * Set isActive 
    * 
    * @param boolean $isActive 
    * 
    * @return User 
    */ 
    public function setIsActive($isActive) 
    { 
     $this->isActive = $isActive; 

     return $this; 
    } 

    /** 
    * Get isActive 
    * 
    * @return boolean 
    */ 
    public function getIsActive() 
    { 
     return $this->isActive; 
    } 

    /** 
    * Add userGroup 
    * 
    * @param \AppBundle\Entity\UserGroup $userGroup 
    * 
    * @return User 
    */ 
    public function addUserGroup(\AppBundle\Entity\UserGroup $userGroup) 
    { 
     $this->userGroups[] = $userGroup; 

     return $this; 
    } 

    /** 
    * Remove userGroup 
    * 
    * @param \AppBundle\Entity\UserGroup $userGroup 
    */ 
    public function removeUserGroup(\AppBundle\Entity\UserGroup $userGroup) 
    { 
     $this->userGroups->removeElement($userGroup); 
    } 

    /** 
    * Get userGroups 
    * 
    * @return \Doctrine\Common\Collections\Collection 
    */ 
    public function getUserGroups() 
    { 
     return $this->userGroups; 
    } 

    /** 
    * Set shibbolethPersistentId 
    * 
    * @param string $shibbolethPersistentId 
    * 
    * @return User 
    */ 
    public function setShibbolethPersistentId($shibbolethPersistentId) 
    { 
     $this->shibbolethPersistentId = $shibbolethPersistentId; 

     return $this; 
    } 

    /** 
    * Get shibbolethPersistentId 
    * 
    * @return string 
    */ 
    public function getShibbolethPersistentId() 
    { 
     return $this->shibbolethPersistentId; 
    } 

    /** 
    * Set firstName 
    * 
    * @param string $firstName 
    * 
    * @return User 
    */ 
    public function setFirstName($firstName) 
    { 
     $this->firstName = $firstName; 

     return $this; 
    } 

    /** 
    * Get firstName 
    * 
    * @return string 
    */ 
    public function getFirstName() 
    { 
     return $this->firstName; 
    } 

    /** 
    * Set lastName 
    * 
    * @param string $lastName 
    * 
    * @return User 
    */ 
    public function setLastName($lastName) 
    { 
     $this->lastName = $lastName; 

     return $this; 
    } 

    /** 
    * Get lastName 
    * 
    * @return string 
    */ 
    public function getLastName() 
    { 
     return $this->lastName; 
    } 

    /** 
    * Set emailConfirmed 
    * 
    * @param boolean $emailConfirmed 
    * 
    * @return User 
    */ 
    public function setEmailConfirmed($emailConfirmed) 
    { 
     $this->emailConfirmed = $emailConfirmed; 

     return $this; 
    } 

    /** 
    * Get emailConfirmed 
    * 
    * @return boolean 
    */ 
    public function getEmailConfirmed() 
    { 
     return $this->emailConfirmed; 
    } 

    public function removeAllUserGroups() { 
     $userGroups = $this->getUserGroups(); 
     foreach($userGroups as $userGroup) { 
      $this->removeUserGroup($userGroup); 
     } 
    } 

    public function hasUserGroup($userGroupId) { 
     foreach($this->getUserGroups() as $userGroup) { 
      if($userGroup->getId() == $userGroupId) 
       return true; 
     } 
     return false; 
    } 

    /** 
    * Set lastLogin 
    * 
    * @param integer $lastLogin 
    * 
    * @return User 
    */ 
    public function setLastLogin($lastLogin) 
    { 
     $this->lastLogin = $lastLogin; 

     return $this; 
    } 

    /** 
    * Get lastLogin 
    * 
    * @return integer 
    */ 
    public function getLastLogin() 
    { 
     return $this->lastLogin; 
    } 

    /** 
    * Set confirmationEmailSend 
    * 
    * @param integer $confirmationEmailSend 
    * 
    * @return User 
    */ 
    public function setConfirmationEmailSend($confirmationEmailSend) 
    { 
     $this->confirmationEmailSend = $confirmationEmailSend; 

     return $this; 
    } 

    /** 
    * Get confirmationEmailSend 
    * 
    * @return integer 
    */ 
    public function getConfirmationEmailSend() 
    { 
     return $this->confirmationEmailSend; 
    } 

    /** 
    * Set validTill 
    * 
    * @param integer $validTill 
    * 
    * @return User 
    */ 
    public function setValidTill($validTill) 
    { 
     $this->validTill = $validTill; 

     return $this; 
    } 

    /** 
    * Get validTill 
    * 
    * @return integer 
    */ 
    public function getValidTill() 
    { 
     return $this->validTill; 
    } 

    /** 
    * Set shibbolethValid 
    * 
    * @param integer $shibbolethValid 
    * 
    * @return User 
    */ 
    public function setShibbolethValid($shibbolethValid) 
    { 
     $this->shibbolethValid = $shibbolethValid; 

     return $this; 
    } 

    /** 
    * Get shibbolethValid 
    * 
    * @return integer 
    */ 
    public function getShibbolethValid() 
    { 
     return $this->shibbolethValid; 
    } 

    /** 
    * Set shibbolethHash 
    * 
    * @param string $shibbolethHash 
    * 
    * @return User 
    */ 
    public function setShibbolethHash($shibbolethHash) 
    { 
     $this->shibbolethHash = $shibbolethHash; 

     return $this; 
    } 

    /** 
    * Get shibbolethHash 
    * 
    * @return string 
    */ 
    public function getShibbolethHash() 
    { 
     return $this->shibbolethHash; 
    } 

    /** 
    * Set shibbolethState 
    * 
    * @param integer $shibbolethState 
    * 
    * @return User 
    */ 
    public function setShibbolethState($shibbolethState) 
    { 
     $this->shibbolethState = $shibbolethState; 

     return $this; 
    } 

    /** 
    * Get shibbolethState 
    * 
    * @return integer 
    */ 
    public function getShibbolethState() 
    { 
     return $this->shibbolethState; 
    } 

    /** 
    * Set expires 
    * 
    * @param integer $expires 
    * 
    * @return User 
    */ 
    public function setExpires($expires) 
    { 
     $this->expires = $expires; 

     return $this; 
    } 

    /** 
    * Get expires 
    * 
    * @return integer 
    */ 
    public function getExpires() 
    { 
     return $this->expires; 
    } 

    /** 
    * Set emailNew 
    * 
    * @param string $emailNew 
    * 
    * @return User 
    */ 
    public function setEmailNew($emailNew) 
    { 
     $this->emailNew = $emailNew; 

     return $this; 
    } 

    /** 
    * Get emailNew 
    * 
    * @return string 
    */ 
    public function getEmailNew() 
    { 
     return $this->emailNew; 
    } 

    /** 
    * Set passwordHash 
    * 
    * @param string $passwordHash 
    * 
    * @return User 
    */ 
    public function setPasswordHash($passwordHash) 
    { 
     $this->passwordHash = $passwordHash; 

     return $this; 
    } 

    /** 
    * Get passwordHash 
    * 
    * @return string 
    */ 
    public function getPasswordHash() 
    { 
     return $this->passwordHash; 
    } 

    /** 
    * Set sessionId 
    * 
    * @param string $sessionId 
    * 
    * @return User 
    */ 
    public function setSessionId($sessionId) 
    { 
     $this->sessionId = $sessionId; 

     return $this; 
    } 

    /** 
    * Get sessionId 
    * 
    * @return string 
    */ 
    public function getSessionId() 
    { 
     return $this->sessionId; 
    } 

    /** 
    * Set salutation 
    * 
    * @param \AppBundle\Entity\Salutation $salutation 
    * 
    * @return User 
    */ 
    public function setSalutation(\AppBundle\Entity\Salutation $salutation = null) 
    { 
     $this->salutation = $salutation; 

     return $this; 
    } 

    /** 
    * Get salutation 
    * 
    * @return \AppBundle\Entity\Salutation 
    */ 
    public function getSalutation() 
    { 
     return $this->salutation; 
    } 

    /** 
    * Add bankDetail 
    * 
    * @param \AppBundle\Entity\BankDetails $bankDetail 
    * 
    * @return User 
    */ 
    public function addBankDetail(\AppBundle\Entity\BankDetails $bankDetail) 
    { 
     $this->bankDetails[] = $bankDetail; 

     return $this; 
    } 

    /** 
    * Remove bankDetail 
    * 
    * @param \AppBundle\Entity\BankDetails $bankDetail 
    */ 
    public function removeBankDetail(\AppBundle\Entity\BankDetails $bankDetail) 
    { 
     $this->bankDetails->removeElement($bankDetail); 
    } 

    /** 
    * Get bankDetails 
    * 
    * @return \Doctrine\Common\Collections\Collection 
    */ 
    public function getBankDetails() 
    { 
     return $this->bankDetails; 
    } 

    /** 
    * Add billingAddress 
    * 
    * @param \AppBundle\Entity\Address $billingAddress 
    * 
    * @return User 
    */ 
    public function addBillingAddress(\AppBundle\Entity\Address $billingAddress) 
    { 
     $this->billingAddresses[] = $billingAddress; 

     return $this; 
    } 

    /** 
    * Remove billingAddress 
    * 
    * @param \AppBundle\Entity\Address $billingAddress 
    */ 
    public function removeBillingAddress(\AppBundle\Entity\Address $billingAddress) 
    { 
     $this->billingAddresses->removeElement($billingAddress); 
    } 

    /** 
    * Set billingAddresses 
    * 
    * @param \AppBundle\Entity\Address $billingAddress 
    * 
    * @return User 
    * 
    */ 
    public function setBillingAddresses(\AppBundle\Entity\Address $billingAddress) 
    { 
     if($this->billingAddresses !== NULL and $this->billingAddresses->contains($billingAddress)){ 
      return false; 
     } 
     $this->addBillingAddress($billingAddress); 
     return $this; 
    } 

    /** 
    * Set one billingAddresses 
    * 
    * @param \AppBundle\Entity\Address $billingAddress 
    * 
    * @return User 
    * 
    */ 
    public function setOneBillingAddresses(\AppBundle\Entity\Address $billingAddress) 
    { 
     $this->billingAddresses = $billingAddress; 

     return $this; 
    } 

    /** 
    * Set one billingAddresses 
    * 
    * @param \AppBundle\Entity\Address $billingAddress 
    * 
    * @return User 
    * 
    */ 
    public function unsetBillingAddresses() 
    { 
     $this->billingAddresses = new ArrayCollection(); 

     return $this; 
    } 

    /** 
    * Get billingAddresses 
    * 
    * @return \Doctrine\Common\Collections\Collection 
    */ 
    public function getBillingAddresses() 
    { 
     return $this->billingAddresses; 
    } 
} 

config/security.yml

providers: 
     chain_provider: 
      chain: 
       providers: [in_memory, database_user] 
     in_memory: 
      memory: 
       users: 
        admin: 
         password: *** 
         roles: 'ROLE_ADMIN' 
     database_user: 
      entity: 
       class: AppBundle:User 

    firewalls: 
     # disables authentication for assets and the profiler, adapt it according to your needs 
     dev: 
      pattern: ^/(_(profiler|wdt)|css|images|js)/ 
      security: false 
     secured_area: 
      # pattern: match to pages 
      anonymous: ~ 
      pattern: ^/ 
      access_denied_handler: AppBundle\Security\AccessDeniedHandler 
      provider: chain_provider 
      form_login: 
       login_path: /login 
       check_path: /login_check 
       default_target_path: account 
       # Configuring CSRF protection 
       csrf_parameter: _csrf_security_token 
       csrf_token_id: a_private_string 
       success_handler: AppBundle\Handler\LoginSuccessHandler 
      logout: 
       path: /logout 
       target: /login 

    access_control: 
     ... 

    role_hierarchy: 
     ... 

    encoders: 
     AppBundle\Entity\User: 
      algorithm: bcrypt 
     Symfony\Component\Security\Core\User\User: 
      plaintext 

Odpowiedz

17

Jak z Symfony 4.0, logout_on_user_change jest ustawiony na true. Oznacza to, że użytkownik zostanie wylogowany, jeśli został zmieniony.

Należy wdrożyć Symfony\Component\Security\Core\User\EquatableInterface i dodać metodę isEqualTo:

class User implements EquatableInterface 
{ 
    public function isEqualTo(UserInterface $user) 
    { 
     if ($this->password !== $user->getPassword()) { 
      return false; 
     } 

     if ($this->salt !== $user->getSalt()) { 
      return false; 
     } 

     if ($this->username !== $user->getUsername()) { 
      return false; 
     } 

     return true; 
    } 
} 

Changelog

https://github.com/symfony/security-bundle/blob/master/CHANGELOG.md

4.1.0

Opti logout_on_user_change firewall on jest przestarzałe i zostanie usunięte w wersji 5.0.

4.0.0

opcja Zapora logout_on_user_change jest zawsze prawdziwe, co spowoduje wylogowanie jeśli użytkownik zmienia między wniosków

3.4.0

Dodane logout_on_user_change do opcje zapory ogniowej. Ten element konfiguracji wyzwoli wylogowanie po zmianie użytkownika. Powinien być ustawiony na wartość true, aby uniknąć przestojów w konfiguracji.

Opcja nie zostały jeszcze udokumentowane: https://github.com/symfony/symfony-docs/issues/8428

dygresja na aktualizację do nowej wersji głównej

Jeśli chcesz dokonać aktualizacji do nowej wersji głównej, zawsze aktualizować do najnowszej moll wersja pierwsza. Oznacza to aktualizację do wersji 2.8 przed aktualizacją do wersji 3.0 i zaktualizowaniem do wersji 3.4 przed przejściem do wersji 4.0. Zobacz Symfony 4: Compose your Applications autorstwa Fabiena Potenciera.

Symfony Symfony 3,0 = 2,8 - nieaktualnych

(..)

Symfony 4,0 = Symfony 3.4 - przestarzałe funkcje + nowy sposób rozwijać aplikacji

aktualizacji do nowej wersji głównej jest o wiele łatwiejsze, jeśli jesteś już na najnowszej mniejszym wydaniu, ponieważ można zobaczyć wszystkie anonse amortyzację.

+0

Wielkie dzięki! Sprawdzałem kod przez wiele godzin, ale niczego nie znalazłem sam! Czy nie powinno to zostać dodane do dokumentacji dostawcy [podmiotu] (https://symfony.com/doc/current/security/entity_provider.html)? – user2625247

+0

Nie ma za co! Ja też szukam godzin, kilka dni temu, tak chętnie dzielę się tym rozwiązaniem. Nie wiem, gdzie powinien być udokumentowany, ale przynajmniej powinien gdzieś być. –

+0

Witam, mam dokładnie ten sam problem, ale to rozwiązanie nie działa :(Dowolny pomysł? – ArGh

Powiązane problemy