Passed
Push — trunk ( 29fffa...ce05d4 )
by Christian
13:01 queued 12s
created

AccountService::fetchCustomer()   B

Complexity

Conditions 9
Paths 2

Size

Total Lines 43
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 17
c 0
b 0
f 0
nc 2
nop 3
dl 0
loc 43
rs 8.0555
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Checkout\Customer\SalesChannel;
4
5
use Shopware\Core\Checkout\Cart\Exception\CustomerNotLoggedInException;
6
use Shopware\Core\Checkout\Customer\CustomerEntity;
0 ignored issues
show
Bug introduced by
The type Shopware\Core\Checkout\Customer\CustomerEntity was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use Shopware\Core\Checkout\Customer\Event\CustomerBeforeLoginEvent;
8
use Shopware\Core\Checkout\Customer\Event\CustomerLoginEvent;
9
use Shopware\Core\Checkout\Customer\Exception\AddressNotFoundException;
10
use Shopware\Core\Checkout\Customer\Exception\BadCredentialsException;
11
use Shopware\Core\Checkout\Customer\Exception\CustomerNotFoundByIdException;
12
use Shopware\Core\Checkout\Customer\Exception\CustomerNotFoundException;
13
use Shopware\Core\Checkout\Customer\Exception\CustomerOptinNotCompletedException;
14
use Shopware\Core\Checkout\Customer\Password\LegacyPasswordVerifier;
15
use Shopware\Core\Framework\Context;
16
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
17
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
18
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
19
use Shopware\Core\Framework\Feature;
20
use Shopware\Core\Framework\Uuid\Exception\InvalidUuidException;
21
use Shopware\Core\Framework\Uuid\Uuid;
22
use Shopware\Core\System\SalesChannel\Context\CartRestorer;
23
use Shopware\Core\System\SalesChannel\SalesChannelContext;
24
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
25
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
26
27
/**
28
 * @package customer-order
29
 */
30
class AccountService
31
{
32
    /**
33
     * @var EntityRepository
34
     */
35
    private $customerRepository;
36
37
    /**
38
     * @var EventDispatcherInterface
39
     */
40
    private $eventDispatcher;
41
42
    /**
43
     * @var LegacyPasswordVerifier
44
     */
45
    private $legacyPasswordVerifier;
46
47
    /**
48
     * @var AbstractSwitchDefaultAddressRoute
49
     */
50
    private $switchDefaultAddressRoute;
51
52
    private CartRestorer $restorer;
53
54
    /**
55
     * @internal
56
     */
57
    public function __construct(
58
        EntityRepository $customerRepository,
59
        EventDispatcherInterface $eventDispatcher,
60
        LegacyPasswordVerifier $legacyPasswordVerifier,
61
        AbstractSwitchDefaultAddressRoute $switchDefaultAddressRoute,
62
        CartRestorer $restorer
63
    ) {
64
        $this->customerRepository = $customerRepository;
65
        $this->eventDispatcher = $eventDispatcher;
66
        $this->legacyPasswordVerifier = $legacyPasswordVerifier;
67
        $this->switchDefaultAddressRoute = $switchDefaultAddressRoute;
68
        $this->restorer = $restorer;
69
    }
70
71
    /**
72
     * @throws CustomerNotLoggedInException
73
     * @throws InvalidUuidException
74
     * @throws AddressNotFoundException
75
     */
76
    public function setDefaultBillingAddress(string $addressId, SalesChannelContext $context, CustomerEntity $customer): void
77
    {
78
        $this->switchDefaultAddressRoute->swap($addressId, AbstractSwitchDefaultAddressRoute::TYPE_BILLING, $context, $customer);
79
    }
80
81
    /**
82
     * @throws CustomerNotLoggedInException
83
     * @throws InvalidUuidException
84
     * @throws AddressNotFoundException
85
     */
86
    public function setDefaultShippingAddress(string $addressId, SalesChannelContext $context, CustomerEntity $customer): void
87
    {
88
        $this->switchDefaultAddressRoute->swap($addressId, AbstractSwitchDefaultAddressRoute::TYPE_SHIPPING, $context, $customer);
89
    }
90
91
    /**
92
     * @throws BadCredentialsException
93
     * @throws UnauthorizedHttpException
94
     */
95
    public function login(string $email, SalesChannelContext $context, bool $includeGuest = false): string
96
    {
97
        if (empty($email)) {
98
            throw new BadCredentialsException();
99
        }
100
101
        $event = new CustomerBeforeLoginEvent($context, $email);
102
        $this->eventDispatcher->dispatch($event);
103
104
        try {
105
            $customer = $this->getCustomerByEmail($email, $context, $includeGuest);
106
        } catch (CustomerNotFoundException $exception) {
107
            throw new UnauthorizedHttpException('json', $exception->getMessage());
108
        }
109
110
        return $this->loginByCustomer($customer, $context);
111
    }
112
113
    /**
114
     * @throws BadCredentialsException
115
     * @throws UnauthorizedHttpException
116
     */
117
    public function loginById(string $id, SalesChannelContext $context): string
118
    {
119
        if (!Uuid::isValid($id)) {
120
            throw new BadCredentialsException();
121
        }
122
123
        try {
124
            $customer = $this->getCustomerById($id, $context);
125
        } catch (CustomerNotFoundByIdException $exception) {
126
            throw new UnauthorizedHttpException('json', $exception->getMessage());
127
        }
128
129
        $event = new CustomerBeforeLoginEvent($context, $customer->getEmail());
130
        $this->eventDispatcher->dispatch($event);
131
132
        return $this->loginByCustomer($customer, $context);
133
    }
134
135
    /**
136
     * @throws CustomerNotFoundException
137
     * @throws BadCredentialsException
138
     * @throws CustomerOptinNotCompletedException
139
     */
140
    public function getCustomerByLogin(string $email, string $password, SalesChannelContext $context): CustomerEntity
141
    {
142
        $customer = $this->getCustomerByEmail($email, $context);
143
144
        if ($customer->hasLegacyPassword()) {
145
            if (!$this->legacyPasswordVerifier->verify($password, $customer)) {
146
                throw new BadCredentialsException();
147
            }
148
149
            $this->updatePasswordHash($password, $customer, $context->getContext());
150
151
            return $customer;
152
        }
153
154
        if ($customer->getPassword() === null
155
            || !password_verify($password, $customer->getPassword())) {
156
            throw new BadCredentialsException();
157
        }
158
159
        if (!$this->isCustomerConfirmed($customer)) {
160
            // Make sure to only throw this exception after it has been verified it was a valid login
161
            throw new CustomerOptinNotCompletedException($customer->getId());
162
        }
163
164
        return $customer;
165
    }
166
167
    private function isCustomerConfirmed(CustomerEntity $customer): bool
168
    {
169
        return !$customer->getDoubleOptInRegistration() || $customer->getDoubleOptInConfirmDate();
170
    }
171
172
    private function loginByCustomer(CustomerEntity $customer, SalesChannelContext $context): string
173
    {
174
        $this->customerRepository->update([
175
            [
176
                'id' => $customer->getId(),
177
                'lastLogin' => new \DateTimeImmutable(),
178
            ],
179
        ], $context->getContext());
180
181
        $context = $this->restorer->restore($customer->getId(), $context);
182
        $newToken = $context->getToken();
183
184
        $event = new CustomerLoginEvent($context, $customer, $newToken);
185
        $this->eventDispatcher->dispatch($event);
186
187
        return $newToken;
188
    }
189
190
    /**
191
     * @throws CustomerNotFoundException
192
     */
193
    private function getCustomerByEmail(string $email, SalesChannelContext $context, bool $includeGuest = false): CustomerEntity
194
    {
195
        $criteria = new Criteria();
196
        $criteria->addFilter(new EqualsFilter('email', $email));
197
198
        $customer = $this->fetchCustomer($criteria, $context, $includeGuest);
199
        if ($customer === null) {
200
            throw new CustomerNotFoundException($email);
201
        }
202
203
        return $customer;
204
    }
205
206
    /**
207
     * @throws CustomerNotFoundByIdException
208
     */
209
    private function getCustomerById(string $id, SalesChannelContext $context): CustomerEntity
210
    {
211
        $criteria = new Criteria([$id]);
212
213
        $customer = $this->fetchCustomer($criteria, $context, true);
214
        if ($customer === null) {
215
            throw new CustomerNotFoundByIdException($id);
216
        }
217
218
        return $customer;
219
    }
220
221
    /**
222
     * This method filters for the standard customer related constraints like active or the sales channel
223
     * assignment.
224
     * Add only filters to the $criteria for values which have an index in the database, e.g. id, or email. The rest
225
     * should be done via PHP because it's a lot faster to filter a few entities on PHP side with the same email
226
     * address, than to filter a huge numbers of rows in the DB on a not indexed column.
227
     */
228
    private function fetchCustomer(Criteria $criteria, SalesChannelContext $context, bool $includeGuest = false): ?CustomerEntity
229
    {
230
        $criteria->setTitle('account-service::fetchCustomer');
231
232
        $result = $this->customerRepository->search($criteria, $context->getContext());
233
        $result = $result->filter(function (CustomerEntity $customer) use ($includeGuest, $context): ?bool {
234
            // Skip not active users
235
            if (!$customer->getActive()) {
236
                // Customers with double opt-in will be active by default starting at Shopware 6.6.0.0,
237
                // remove complete if statement and always return null
238
                if (Feature::isActive('v6.6.0.0') || $this->isCustomerConfirmed($customer)) {
239
                    return null;
240
                }
241
            }
242
243
            // Skip guest if not required
244
            if (!$includeGuest && $customer->getGuest()) {
245
                return null;
246
            }
247
248
            // If not bound, we still need to consider it
249
            if ($customer->getBoundSalesChannelId() === null) {
250
                return true;
251
            }
252
253
            // It is bound, but not to the current one. Skip it
254
            if ($customer->getBoundSalesChannelId() !== $context->getSalesChannel()->getId()) {
255
                return null;
256
            }
257
258
            return true;
259
        });
260
261
        // If there is more than one account we want to return the latest, this is important
262
        // for guest accounts, real customer accounts should only occur once, otherwise the
263
        // wrong password will be validated
264
        if ($result->count() > 1) {
265
            $result->sort(function (CustomerEntity $a, CustomerEntity $b) {
266
                return ($a->getCreatedAt() <=> $b->getCreatedAt()) * -1;
267
            });
268
        }
269
270
        return $result->first();
271
    }
272
273
    private function updatePasswordHash(string $password, CustomerEntity $customer, Context $context): void
274
    {
275
        $this->customerRepository->update([
276
            [
277
                'id' => $customer->getId(),
278
                'password' => $password,
279
                'legacyPassword' => null,
280
                'legacyEncoder' => null,
281
            ],
282
        ], $context);
283
    }
284
}
285