· 8 years ago · May 26, 2018, 01:42 PM
1<?php
2/**
3 * Copyright © Magento, Inc. All rights reserved.
4 * See COPYING.txt for license details.
5 */
6namespace Magento\Customer\Model;
7
8use Magento\Customer\Api\AccountManagementInterface;
9use Magento\Customer\Api\AddressRepositoryInterface;
10use Magento\Customer\Api\CustomerMetadataInterface;
11use Magento\Customer\Api\CustomerRepositoryInterface;
12use Magento\Customer\Api\Data\AddressInterface;
13use Magento\Customer\Api\Data\CustomerInterface;
14use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory;
15use Magento\Customer\Helper\View as CustomerViewHelper;
16use Magento\Customer\Model\Config\Share as ConfigShare;
17use Magento\Customer\Model\Customer as CustomerModel;
18use Magento\Customer\Model\Customer\CredentialsValidator;
19use Magento\Customer\Model\Metadata\Validator;
20use Magento\Eav\Model\Validator\Attribute\Backend;
21use Magento\Framework\Api\ExtensibleDataObjectConverter;
22use Magento\Framework\App\Area;
23use Magento\Framework\App\Config\ScopeConfigInterface;
24use Magento\Framework\App\ObjectManager;
25use Magento\Framework\DataObjectFactory as ObjectFactory;
26use Magento\Framework\Encryption\EncryptorInterface as Encryptor;
27use Magento\Framework\Encryption\Helper\Security;
28use Magento\Framework\Event\ManagerInterface;
29use Magento\Framework\Exception\AlreadyExistsException;
30use Magento\Framework\Exception\EmailNotConfirmedException;
31use Magento\Framework\Exception\InputException;
32use Magento\Framework\Exception\InvalidEmailOrPasswordException;
33use Magento\Framework\Exception\LocalizedException;
34use Magento\Framework\Exception\MailException;
35use Magento\Framework\Exception\NoSuchEntityException;
36use Magento\Framework\Exception\State\ExpiredException;
37use Magento\Framework\Exception\State\InputMismatchException;
38use Magento\Framework\Exception\State\InvalidTransitionException;
39use Magento\Framework\Exception\State\UserLockedException;
40use Magento\Framework\Intl\DateTimeFactory;
41use Magento\Framework\Mail\Template\TransportBuilder;
42use Magento\Framework\Math\Random;
43use Magento\Framework\Reflection\DataObjectProcessor;
44use Magento\Framework\Registry;
45use Magento\Framework\Stdlib\DateTime;
46use Magento\Framework\Stdlib\StringUtils as StringHelper;
47use Magento\Store\Model\ScopeInterface;
48use Magento\Store\Model\StoreManagerInterface;
49use Psr\Log\LoggerInterface as PsrLogger;
50use Magento\Framework\Session\SessionManagerInterface;
51use Magento\Framework\Session\SaveHandlerInterface;
52use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory;
53
54/**
55 * Handle various customer account actions
56 *
57 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
58 * @SuppressWarnings(PHPMD.TooManyFields)
59 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
60 */
61class AccountManagement implements AccountManagementInterface
62{
63 /**
64 * Configuration paths for email templates and identities
65 *
66 * @deprecated
67 */
68 const XML_PATH_REGISTER_EMAIL_TEMPLATE = 'customer/create_account/email_template';
69
70 /**
71 * @deprecated
72 */
73 const XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE = 'customer/create_account/email_no_password_template';
74
75 /**
76 * @deprecated
77 */
78 const XML_PATH_REGISTER_EMAIL_IDENTITY = 'customer/create_account/email_identity';
79
80 /**
81 * @deprecated
82 */
83 const XML_PATH_REMIND_EMAIL_TEMPLATE = 'customer/password/remind_email_template';
84
85 /**
86 * @deprecated
87 */
88 const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'customer/password/forgot_email_template';
89
90 /**
91 * @deprecated
92 */
93 const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity';
94
95 const XML_PATH_IS_CONFIRM = 'customer/create_account/confirm';
96
97 /**
98 * @deprecated
99 */
100 const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template';
101
102 /**
103 * @deprecated
104 */
105 const XML_PATH_CONFIRMED_EMAIL_TEMPLATE = 'customer/create_account/email_confirmed_template';
106
107 /**
108 * Constants for the type of new account email to be sent
109 *
110 * @deprecated
111 */
112 const NEW_ACCOUNT_EMAIL_REGISTERED = 'registered';
113
114 /**
115 * Welcome email, when password setting is required
116 *
117 * @deprecated
118 */
119 const NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD = 'registered_no_password';
120
121 /**
122 * Welcome email, when confirmation is enabled
123 *
124 * @deprecated
125 */
126 const NEW_ACCOUNT_EMAIL_CONFIRMATION = 'confirmation';
127
128 /**
129 * Confirmation email, when account is confirmed
130 *
131 * @deprecated
132 */
133 const NEW_ACCOUNT_EMAIL_CONFIRMED = 'confirmed';
134
135 /**
136 * Constants for types of emails to send out.
137 * pdl:
138 * forgot, remind, reset email templates
139 */
140 const EMAIL_REMINDER = 'email_reminder';
141
142 const EMAIL_RESET = 'email_reset';
143
144 /**
145 * Configuration path to customer password minimum length
146 */
147 const XML_PATH_MINIMUM_PASSWORD_LENGTH = 'customer/password/minimum_password_length';
148
149 /**
150 * Configuration path to customer password required character classes number
151 */
152 const XML_PATH_REQUIRED_CHARACTER_CLASSES_NUMBER = 'customer/password/required_character_classes_number';
153
154 /**
155 * @deprecated
156 */
157 const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template';
158
159 /**
160 * @deprecated
161 */
162 const MIN_PASSWORD_LENGTH = 6;
163
164 /**
165 * @var CustomerFactory
166 */
167 private $customerFactory;
168
169 /**
170 * @var \Magento\Customer\Api\Data\ValidationResultsInterfaceFactory
171 */
172 private $validationResultsDataFactory;
173
174 /**
175 * @var ManagerInterface
176 */
177 private $eventManager;
178
179 /**
180 * @var \Magento\Store\Model\StoreManagerInterface
181 */
182 private $storeManager;
183
184 /**
185 * @var Random
186 */
187 private $mathRandom;
188
189 /**
190 * @var Validator
191 */
192 private $validator;
193
194 /**
195 * @var AddressRepositoryInterface
196 */
197 private $addressRepository;
198
199 /**
200 * @var CustomerMetadataInterface
201 */
202 private $customerMetadataService;
203
204 /**
205 * @var PsrLogger
206 */
207 protected $logger;
208
209 /**
210 * @var Encryptor
211 */
212 private $encryptor;
213
214 /**
215 * @var CustomerRegistry
216 */
217 private $customerRegistry;
218
219 /**
220 * @var ConfigShare
221 */
222 private $configShare;
223
224 /**
225 * @var StringHelper
226 */
227 protected $stringHelper;
228
229 /**
230 * @var CustomerRepositoryInterface
231 */
232 private $customerRepository;
233
234 /**
235 * @var ScopeConfigInterface
236 */
237 private $scopeConfig;
238
239 /**
240 * @var TransportBuilder
241 */
242 private $transportBuilder;
243
244 /**
245 * @var SessionManagerInterface
246 */
247 private $sessionManager;
248
249 /**
250 * @var SaveHandlerInterface
251 */
252 private $saveHandler;
253
254 /**
255 * @var CollectionFactory
256 */
257 private $visitorCollectionFactory;
258
259 /**
260 * @var DataObjectProcessor
261 */
262 protected $dataProcessor;
263
264 /**
265 * @var \Magento\Framework\Registry
266 */
267 protected $registry;
268
269 /**
270 * @var CustomerViewHelper
271 */
272 protected $customerViewHelper;
273
274 /**
275 * @var DateTime
276 */
277 protected $dateTime;
278
279 /**
280 * @var ObjectFactory
281 */
282 protected $objectFactory;
283
284 /**
285 * @var \Magento\Framework\Api\ExtensibleDataObjectConverter
286 */
287 protected $extensibleDataObjectConverter;
288
289 /**
290 * @var CustomerModel
291 */
292 protected $customerModel;
293
294 /**
295 * @var AuthenticationInterface
296 */
297 protected $authentication;
298
299 /**
300 * @var EmailNotificationInterface
301 */
302 private $emailNotification;
303
304 /**
305 * @var \Magento\Eav\Model\Validator\Attribute\Backend
306 */
307 private $eavValidator;
308
309 /**
310 * @var CredentialsValidator
311 */
312 private $credentialsValidator;
313
314 /**
315 * @var DateTimeFactory
316 */
317 private $dateTimeFactory;
318
319 /**
320 * @param CustomerFactory $customerFactory
321 * @param ManagerInterface $eventManager
322 * @param StoreManagerInterface $storeManager
323 * @param Random $mathRandom
324 * @param Validator $validator
325 * @param ValidationResultsInterfaceFactory $validationResultsDataFactory
326 * @param AddressRepositoryInterface $addressRepository
327 * @param CustomerMetadataInterface $customerMetadataService
328 * @param CustomerRegistry $customerRegistry
329 * @param PsrLogger $logger
330 * @param Encryptor $encryptor
331 * @param ConfigShare $configShare
332 * @param StringHelper $stringHelper
333 * @param CustomerRepositoryInterface $customerRepository
334 * @param ScopeConfigInterface $scopeConfig
335 * @param TransportBuilder $transportBuilder
336 * @param DataObjectProcessor $dataProcessor
337 * @param Registry $registry
338 * @param CustomerViewHelper $customerViewHelper
339 * @param DateTime $dateTime
340 * @param CustomerModel $customerModel
341 * @param ObjectFactory $objectFactory
342 * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter
343 * @param CredentialsValidator|null $credentialsValidator
344 * @param DateTimeFactory $dateTimeFactory
345 * @param SessionManagerInterface|null $sessionManager
346 * @param SaveHandlerInterface|null $saveHandler
347 * @param CollectionFactory|null $visitorCollectionFactory
348 * @SuppressWarnings(PHPMD.ExcessiveParameterList)
349 */
350 public function __construct(
351 CustomerFactory $customerFactory,
352 ManagerInterface $eventManager,
353 StoreManagerInterface $storeManager,
354 Random $mathRandom,
355 Validator $validator,
356 ValidationResultsInterfaceFactory $validationResultsDataFactory,
357 AddressRepositoryInterface $addressRepository,
358 CustomerMetadataInterface $customerMetadataService,
359 CustomerRegistry $customerRegistry,
360 PsrLogger $logger,
361 Encryptor $encryptor,
362 ConfigShare $configShare,
363 StringHelper $stringHelper,
364 CustomerRepositoryInterface $customerRepository,
365 ScopeConfigInterface $scopeConfig,
366 TransportBuilder $transportBuilder,
367 DataObjectProcessor $dataProcessor,
368 Registry $registry,
369 CustomerViewHelper $customerViewHelper,
370 DateTime $dateTime,
371 CustomerModel $customerModel,
372 ObjectFactory $objectFactory,
373 ExtensibleDataObjectConverter $extensibleDataObjectConverter,
374 CredentialsValidator $credentialsValidator = null,
375 DateTimeFactory $dateTimeFactory = null,
376 SessionManagerInterface $sessionManager = null,
377 SaveHandlerInterface $saveHandler = null,
378 CollectionFactory $visitorCollectionFactory = null
379 ) {
380 $this->customerFactory = $customerFactory;
381 $this->eventManager = $eventManager;
382 $this->storeManager = $storeManager;
383 $this->mathRandom = $mathRandom;
384 $this->validator = $validator;
385 $this->validationResultsDataFactory = $validationResultsDataFactory;
386 $this->addressRepository = $addressRepository;
387 $this->customerMetadataService = $customerMetadataService;
388 $this->customerRegistry = $customerRegistry;
389 $this->logger = $logger;
390 $this->encryptor = $encryptor;
391 $this->configShare = $configShare;
392 $this->stringHelper = $stringHelper;
393 $this->customerRepository = $customerRepository;
394 $this->scopeConfig = $scopeConfig;
395 $this->transportBuilder = $transportBuilder;
396 $this->dataProcessor = $dataProcessor;
397 $this->registry = $registry;
398 $this->customerViewHelper = $customerViewHelper;
399 $this->dateTime = $dateTime;
400 $this->customerModel = $customerModel;
401 $this->objectFactory = $objectFactory;
402 $this->extensibleDataObjectConverter = $extensibleDataObjectConverter;
403 $this->credentialsValidator =
404 $credentialsValidator ?: ObjectManager::getInstance()->get(CredentialsValidator::class);
405 $this->dateTimeFactory = $dateTimeFactory ?: ObjectManager::getInstance()->get(DateTimeFactory::class);
406 $this->sessionManager = $sessionManager
407 ?: ObjectManager::getInstance()->get(SessionManagerInterface::class);
408 $this->saveHandler = $saveHandler
409 ?: ObjectManager::getInstance()->get(SaveHandlerInterface::class);
410 $this->visitorCollectionFactory = $visitorCollectionFactory
411 ?: ObjectManager::getInstance()->get(CollectionFactory::class);
412 }
413
414 /**
415 * Get authentication
416 *
417 * @return AuthenticationInterface
418 */
419 private function getAuthentication()
420 {
421 if (!($this->authentication instanceof AuthenticationInterface)) {
422 return \Magento\Framework\App\ObjectManager::getInstance()->get(
423 \Magento\Customer\Model\AuthenticationInterface::class
424 );
425 } else {
426 return $this->authentication;
427 }
428 }
429
430 /**
431 * {@inheritdoc}
432 */
433 public function resendConfirmation($email, $websiteId = null, $redirectUrl = '')
434 {
435 $customer = $this->customerRepository->get($email, $websiteId);
436 if (!$customer->getConfirmation()) {
437 throw new InvalidTransitionException(__('No confirmation needed.'));
438 }
439
440 try {
441 $this->getEmailNotification()->newAccount(
442 $customer,
443 self::NEW_ACCOUNT_EMAIL_CONFIRMATION,
444 $redirectUrl,
445 $this->storeManager->getStore()->getId()
446 );
447 } catch (MailException $e) {
448 // If we are not able to send a new account email, this should be ignored
449 $this->logger->critical($e);
450 }
451 }
452
453 /**
454 * {@inheritdoc}
455 */
456 public function activate($email, $confirmationKey)
457 {
458 $customer = $this->customerRepository->get($email);
459 return $this->activateCustomer($customer, $confirmationKey);
460 }
461
462 /**
463 * {@inheritdoc}
464 */
465 public function activateById($customerId, $confirmationKey)
466 {
467 $customer = $this->customerRepository->getById($customerId);
468 return $this->activateCustomer($customer, $confirmationKey);
469 }
470
471 /**
472 * Activate a customer account using a key that was sent in a confirmation email.
473 *
474 * @param \Magento\Customer\Api\Data\CustomerInterface $customer
475 * @param string $confirmationKey
476 * @return \Magento\Customer\Api\Data\CustomerInterface
477 * @throws \Magento\Framework\Exception\State\InvalidTransitionException
478 * @throws \Magento\Framework\Exception\State\InputMismatchException
479 */
480 private function activateCustomer($customer, $confirmationKey)
481 {
482 // check if customer is inactive
483 if (!$customer->getConfirmation()) {
484 throw new InvalidTransitionException(__('Account already active'));
485 }
486
487 if ($customer->getConfirmation() !== $confirmationKey) {
488 throw new InputMismatchException(__('Invalid confirmation token'));
489 }
490
491 $customer->setConfirmation(null);
492 $this->customerRepository->save($customer);
493 $this->getEmailNotification()->newAccount($customer, 'confirmed', '', $this->storeManager->getStore()->getId());
494 return $customer;
495 }
496
497 /**
498 * {@inheritdoc}
499 */
500 public function authenticate($username, $password)
501 {
502 try {
503 $customer = $this->customerRepository->get($username);
504 } catch (NoSuchEntityException $e) {
505 throw new InvalidEmailOrPasswordException(__('Invalid login or password.'));
506 }
507
508 $customerId = $customer->getId();
509 if ($this->getAuthentication()->isLocked($customerId)) {
510 throw new UserLockedException(__('The account is locked.'));
511 }
512 try {
513 $this->getAuthentication()->authenticate($customerId, $password);
514 } catch (InvalidEmailOrPasswordException $e) {
515 throw new InvalidEmailOrPasswordException(__('Invalid login or password.'));
516 }
517 if ($customer->getConfirmation() && $this->isConfirmationRequired($customer)) {
518 throw new EmailNotConfirmedException(__('This account is not confirmed.'));
519 }
520
521 $customerModel = $this->customerFactory->create()->updateData($customer);
522 $this->eventManager->dispatch(
523 'customer_customer_authenticated',
524 ['model' => $customerModel, 'password' => $password]
525 );
526
527 $this->eventManager->dispatch('customer_data_object_login', ['customer' => $customer]);
528
529 return $customer;
530 }
531
532 /**
533 * {@inheritdoc}
534 */
535 public function validateResetPasswordLinkToken($customerId, $resetPasswordLinkToken)
536 {
537 $this->validateResetPasswordToken($customerId, $resetPasswordLinkToken);
538 return true;
539 }
540
541 /**
542 * {@inheritdoc}
543 */
544 public function initiatePasswordReset($email, $template, $websiteId = null)
545 {
546 if ($websiteId === null) {
547 $websiteId = $this->storeManager->getStore()->getWebsiteId();
548 }
549 // load customer by email
550 $customer = $this->customerRepository->get($email, $websiteId);
551
552 $newPasswordToken = $this->mathRandom->getUniqueHash();
553 $this->changeResetPasswordLinkToken($customer, $newPasswordToken);
554
555 try {
556 switch ($template) {
557 case AccountManagement::EMAIL_REMINDER:
558 $this->getEmailNotification()->passwordReminder($customer);
559 break;
560 case AccountManagement::EMAIL_RESET:
561 $this->getEmailNotification()->passwordResetConfirmation($customer);
562 break;
563 default:
564 throw new InputException(__(
565 'Invalid value of "%value" provided for the %fieldName field. '.
566 'Possible values: %template1 or %template2.',
567 [
568 'value' => $template,
569 'fieldName' => 'template',
570 'template1' => AccountManagement::EMAIL_REMINDER,
571 'template2' => AccountManagement::EMAIL_RESET
572 ]
573 ));
574 }
575
576 return true;
577 } catch (MailException $e) {
578 // If we are not able to send a reset password email, this should be ignored
579 $this->logger->critical($e);
580 }
581
582 return false;
583 }
584
585 /**
586 * {@inheritdoc}
587 */
588 public function resetPassword($email, $resetToken, $newPassword)
589 {
590 $customer = $this->customerRepository->get($email);
591 //Validate Token and new password strength
592 $this->validateResetPasswordToken($customer->getId(), $resetToken);
593 $this->checkPasswordStrength($newPassword);
594 //Update secure data
595 $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId());
596 $customerSecure->setRpToken(null);
597 $customerSecure->setRpTokenCreatedAt(null);
598 $customerSecure->setPasswordHash($this->createPasswordHash($newPassword));
599 $this->sessionManager->destroy();
600 $this->destroyCustomerSessions($customer->getId());
601 $this->customerRepository->save($customer);
602
603 return true;
604 }
605
606 /**
607 * Make sure that password complies with minimum security requirements.
608 *
609 * @param string $password
610 * @return void
611 * @throws InputException
612 */
613 protected function checkPasswordStrength($password)
614 {
615 $length = $this->stringHelper->strlen($password);
616 if ($length > self::MAX_PASSWORD_LENGTH) {
617 throw new InputException(
618 __(
619 'Please enter a password with at most %1 characters.',
620 self::MAX_PASSWORD_LENGTH
621 )
622 );
623 }
624 $configMinPasswordLength = $this->getMinPasswordLength();
625 if ($length < $configMinPasswordLength) {
626 throw new InputException(
627 __(
628 'Please enter a password with at least %1 characters.',
629 $configMinPasswordLength
630 )
631 );
632 }
633 if ($this->stringHelper->strlen(trim($password)) != $length) {
634 throw new InputException(__('The password can\'t begin or end with a space.'));
635 }
636
637 $requiredCharactersCheck = $this->makeRequiredCharactersCheck($password);
638 if ($requiredCharactersCheck !== 0) {
639 throw new InputException(
640 __(
641 'Minimum of different classes of characters in password is %1.' .
642 ' Classes of characters: Lower Case, Upper Case, Digits, Special Characters.',
643 $requiredCharactersCheck
644 )
645 );
646 }
647 }
648
649 /**
650 * Check password for presence of required character sets
651 *
652 * @param string $password
653 * @return int
654 */
655 protected function makeRequiredCharactersCheck($password)
656 {
657 $counter = 0;
658 $requiredNumber = $this->scopeConfig->getValue(self::XML_PATH_REQUIRED_CHARACTER_CLASSES_NUMBER);
659 $return = 0;
660
661 if (preg_match('/[0-9]+/', $password)) {
662 $counter++;
663 }
664 if (preg_match('/[A-Z]+/', $password)) {
665 $counter++;
666 }
667 if (preg_match('/[a-z]+/', $password)) {
668 $counter++;
669 }
670 if (preg_match('/[^a-zA-Z0-9]+/', $password)) {
671 $counter++;
672 }
673
674 if ($counter < $requiredNumber) {
675 $return = $requiredNumber;
676 }
677
678 return $return;
679 }
680
681 /**
682 * Retrieve minimum password length
683 *
684 * @return int
685 */
686 protected function getMinPasswordLength()
687 {
688 return $this->scopeConfig->getValue(self::XML_PATH_MINIMUM_PASSWORD_LENGTH);
689 }
690
691 /**
692 * {@inheritdoc}
693 */
694 public function getConfirmationStatus($customerId)
695 {
696 // load customer by id
697 $customer = $this->customerRepository->getById($customerId);
698 if ($this->isConfirmationRequired($customer)) {
699 if (!$customer->getConfirmation()) {
700 return self::ACCOUNT_CONFIRMED;
701 }
702 return self::ACCOUNT_CONFIRMATION_REQUIRED;
703 }
704 return self::ACCOUNT_CONFIRMATION_NOT_REQUIRED;
705 }
706
707 /**
708 * {@inheritdoc}
709 */
710 public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '')
711 {
712 if ($password !== null) {
713 $this->checkPasswordStrength($password);
714 $customerEmail = $customer->getEmail();
715 try {
716 $this->credentialsValidator->checkPasswordDifferentFromEmail($customerEmail, $password);
717 } catch (InputException $e) {
718 throw new LocalizedException(__('Password cannot be the same as email address.'));
719 }
720 $hash = $this->createPasswordHash($password);
721 } else {
722 $hash = null;
723 }
724 return $this->createAccountWithPasswordHash($customer, $hash, $redirectUrl);
725 }
726
727 /**
728 * {@inheritdoc}
729 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
730 * @SuppressWarnings(PHPMD.NPathComplexity)
731 */
732 public function createAccountWithPasswordHash(CustomerInterface $customer, $hash, $redirectUrl = '')
733 {
734 // This logic allows an existing customer to be added to a different store. No new account is created.
735 // The plan is to move this logic into a new method called something like 'registerAccountWithStore'
736 if ($customer->getId()) {
737 $customer = $this->customerRepository->get($customer->getEmail());
738 $websiteId = $customer->getWebsiteId();
739
740 if ($this->isCustomerInStore($websiteId, $customer->getStoreId())) {
741 throw new InputException(__('This customer already exists in this store.'));
742 }
743 // Existing password hash will be used from secured customer data registry when saving customer
744 }
745
746 // Make sure we have a storeId to associate this customer with.
747 if (!$customer->getStoreId()) {
748 if ($customer->getWebsiteId()) {
749 $storeId = $this->storeManager->getWebsite($customer->getWebsiteId())->getDefaultStore()->getId();
750 } else {
751 $storeId = $this->storeManager->getStore()->getId();
752 }
753 $customer->setStoreId($storeId);
754 }
755
756 // Associate website_id with customer
757 if (!$customer->getWebsiteId()) {
758 $websiteId = $this->storeManager->getStore($customer->getStoreId())->getWebsiteId();
759 $customer->setWebsiteId($websiteId);
760 }
761
762 // Update 'created_in' value with actual store name
763 if ($customer->getId() === null) {
764 $storeName = $this->storeManager->getStore($customer->getStoreId())->getName();
765 $customer->setCreatedIn($storeName);
766 }
767
768 $customerAddresses = $customer->getAddresses() ?: [];
769 $customer->setAddresses(null);
770 try {
771 // If customer exists existing hash will be used by Repository
772 $customer = $this->customerRepository->save($customer, $hash);
773 } catch (AlreadyExistsException $e) {
774 throw new InputMismatchException(
775 __('A customer with the same email already exists in an associated website.')
776 );
777 } catch (LocalizedException $e) {
778 throw $e;
779 }
780 try {
781 foreach ($customerAddresses as $address) {
782 if ($address->getId()) {
783 $newAddress = clone $address;
784 $newAddress->setId(null);
785 $newAddress->setCustomerId($customer->getId());
786 $this->addressRepository->save($newAddress);
787 } else {
788 $address->setCustomerId($customer->getId());
789 $this->addressRepository->save($address);
790 }
791 }
792 $this->customerRegistry->remove($customer->getId());
793 } catch (InputException $e) {
794 $this->customerRepository->delete($customer);
795 throw $e;
796 }
797 $customer = $this->customerRepository->getById($customer->getId());
798 $newLinkToken = $this->mathRandom->getUniqueHash();
799 $this->changeResetPasswordLinkToken($customer, $newLinkToken);
800 $this->sendEmailConfirmation($customer, $redirectUrl);
801
802 return $customer;
803 }
804
805 /**
806 * {@inheritdoc}
807 */
808 public function getDefaultBillingAddress($customerId)
809 {
810 $customer = $this->customerRepository->getById($customerId);
811 return $this->getAddressById($customer, $customer->getDefaultBilling());
812 }
813
814 /**
815 * {@inheritdoc}
816 */
817 public function getDefaultShippingAddress($customerId)
818 {
819 $customer = $this->customerRepository->getById($customerId);
820 return $this->getAddressById($customer, $customer->getDefaultShipping());
821 }
822
823 /**
824 * Send either confirmation or welcome email after an account creation
825 *
826 * @param CustomerInterface $customer
827 * @param string $redirectUrl
828 * @return void
829 */
830 protected function sendEmailConfirmation(CustomerInterface $customer, $redirectUrl)
831 {
832 try {
833 $hash = $this->customerRegistry->retrieveSecureData($customer->getId())->getPasswordHash();
834 $templateType = self::NEW_ACCOUNT_EMAIL_REGISTERED;
835 if ($this->isConfirmationRequired($customer) && $hash != '') {
836 $templateType = self::NEW_ACCOUNT_EMAIL_CONFIRMATION;
837 } elseif ($hash == '') {
838 $templateType = self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD;
839 }
840 $this->getEmailNotification()->newAccount($customer, $templateType, $redirectUrl, $customer->getStoreId());
841 } catch (MailException $e) {
842 // If we are not able to send a new account email, this should be ignored
843 $this->logger->critical($e);
844 }
845 }
846
847 /**
848 * {@inheritdoc}
849 */
850 public function changePassword($email, $currentPassword, $newPassword)
851 {
852 try {
853 $customer = $this->customerRepository->get($email);
854 } catch (NoSuchEntityException $e) {
855 throw new InvalidEmailOrPasswordException(__('Invalid login or password.'));
856 }
857 return $this->changePasswordForCustomer($customer, $currentPassword, $newPassword);
858 }
859
860 /**
861 * {@inheritdoc}
862 */
863 public function changePasswordById($customerId, $currentPassword, $newPassword)
864 {
865 try {
866 $customer = $this->customerRepository->getById($customerId);
867 } catch (NoSuchEntityException $e) {
868 throw new InvalidEmailOrPasswordException(__('Invalid login or password.'));
869 }
870 return $this->changePasswordForCustomer($customer, $currentPassword, $newPassword);
871 }
872
873 /**
874 * Change customer password
875 *
876 * @param CustomerInterface $customer
877 * @param string $currentPassword
878 * @param string $newPassword
879 * @return bool true on success
880 * @throws InputException
881 * @throws InvalidEmailOrPasswordException
882 * @throws UserLockedException
883 */
884 private function changePasswordForCustomer($customer, $currentPassword, $newPassword)
885 {
886 try {
887 $this->getAuthentication()->authenticate($customer->getId(), $currentPassword);
888 } catch (InvalidEmailOrPasswordException $e) {
889 throw new InvalidEmailOrPasswordException(__('The password doesn\'t match this account.'));
890 }
891 $customerEmail = $customer->getEmail();
892 $this->credentialsValidator->checkPasswordDifferentFromEmail($customerEmail, $newPassword);
893 $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId());
894 $customerSecure->setRpToken(null);
895 $customerSecure->setRpTokenCreatedAt(null);
896 $this->checkPasswordStrength($newPassword);
897 $customerSecure->setPasswordHash($this->createPasswordHash($newPassword));
898 $this->destroyCustomerSessions($customer->getId());
899 $this->customerRepository->save($customer);
900
901 return true;
902 }
903
904 /**
905 * Create a hash for the given password
906 *
907 * @param string $password
908 * @return string
909 */
910 protected function createPasswordHash($password)
911 {
912 return $this->encryptor->getHash($password, true);
913 }
914
915 /**
916 * @return Backend
917 */
918 private function getEavValidator()
919 {
920 if ($this->eavValidator === null) {
921 $this->eavValidator = ObjectManager::getInstance()->get(Backend::class);
922 }
923 return $this->eavValidator;
924 }
925
926 /**
927 * {@inheritdoc}
928 */
929 public function validate(CustomerInterface $customer)
930 {
931 $validationResults = $this->validationResultsDataFactory->create();
932
933 $oldAddresses = $customer->getAddresses();
934 $customerModel = $this->customerFactory->create()->updateData(
935 $customer->setAddresses([])
936 );
937 $customer->setAddresses($oldAddresses);
938
939 $result = $this->getEavValidator()->isValid($customerModel);
940 if ($result === false && is_array($this->getEavValidator()->getMessages())) {
941 return $validationResults->setIsValid(false)->setMessages(
942 call_user_func_array(
943 'array_merge',
944 $this->getEavValidator()->getMessages()
945 )
946 );
947 }
948 return $validationResults->setIsValid(true)->setMessages([]);
949 }
950
951 /**
952 * {@inheritdoc}
953 */
954 public function isEmailAvailable($customerEmail, $websiteId = null)
955 {
956 try {
957 if ($websiteId === null) {
958 $websiteId = $this->storeManager->getStore()->getWebsiteId();
959 }
960 $this->customerRepository->get($customerEmail, $websiteId);
961 return false;
962 } catch (NoSuchEntityException $e) {
963 return true;
964 }
965 }
966
967 /**
968 * {@inheritDoc}
969 */
970 public function isCustomerInStore($customerWebsiteId, $storeId)
971 {
972 $ids = [];
973 if ((bool)$this->configShare->isWebsiteScope()) {
974 $ids = $this->storeManager->getWebsite($customerWebsiteId)->getStoreIds();
975 } else {
976 foreach ($this->storeManager->getStores() as $store) {
977 $ids[] = $store->getId();
978 }
979 }
980
981 return in_array($storeId, $ids);
982 }
983
984 /**
985 * Validate the Reset Password Token for a customer.
986 *
987 * @param int $customerId
988 * @param string $resetPasswordLinkToken
989 * @return bool
990 * @throws \Magento\Framework\Exception\State\InputMismatchException If token is mismatched
991 * @throws \Magento\Framework\Exception\State\ExpiredException If token is expired
992 * @throws \Magento\Framework\Exception\InputException If token or customer id is invalid
993 * @throws \Magento\Framework\Exception\NoSuchEntityException If customer doesn't exist
994 */
995 private function validateResetPasswordToken($customerId, $resetPasswordLinkToken)
996 {
997 if (empty($customerId) || $customerId < 0) {
998 throw new InputException(
999 __(
1000 'Invalid value of "%value" provided for the %fieldName field.',
1001 ['value' => $customerId, 'fieldName' => 'customerId']
1002 )
1003 );
1004 }
1005 if (!is_string($resetPasswordLinkToken) || empty($resetPasswordLinkToken)) {
1006 $params = ['fieldName' => 'resetPasswordLinkToken'];
1007 throw new InputException(__('%fieldName is a required field.', $params));
1008 }
1009
1010 $customerSecureData = $this->customerRegistry->retrieveSecureData($customerId);
1011 $rpToken = $customerSecureData->getRpToken();
1012 $rpTokenCreatedAt = $customerSecureData->getRpTokenCreatedAt();
1013
1014 if (!Security::compareStrings($rpToken, $resetPasswordLinkToken)) {
1015 throw new InputMismatchException(__('Reset password token mismatch.'));
1016 } elseif ($this->isResetPasswordLinkTokenExpired($rpToken, $rpTokenCreatedAt)) {
1017 throw new ExpiredException(__('Reset password token expired.'));
1018 }
1019
1020 return true;
1021 }
1022
1023 /**
1024 * Check if customer can be deleted.
1025 *
1026 * @param int $customerId
1027 * @return bool
1028 * @throws \Magento\Framework\Exception\NoSuchEntityException If group is not found
1029 * @throws LocalizedException
1030 */
1031 public function isReadonly($customerId)
1032 {
1033 $customer = $this->customerRegistry->retrieveSecureData($customerId);
1034 return !$customer->getDeleteable();
1035 }
1036
1037 /**
1038 * Send email with new account related information
1039 *
1040 * @param CustomerInterface $customer
1041 * @param string $type
1042 * @param string $backUrl
1043 * @param string $storeId
1044 * @param string $sendemailStoreId
1045 * @return $this
1046 * @throws LocalizedException
1047 * @deprecated 100.1.0
1048 */
1049 protected function sendNewAccountEmail(
1050 $customer,
1051 $type = self::NEW_ACCOUNT_EMAIL_REGISTERED,
1052 $backUrl = '',
1053 $storeId = '0',
1054 $sendemailStoreId = null
1055 ) {
1056 $types = $this->getTemplateTypes();
1057
1058 if (!isset($types[$type])) {
1059 throw new LocalizedException(__('Please correct the transactional account email type.'));
1060 }
1061
1062 if (!$storeId) {
1063 $storeId = $this->getWebsiteStoreId($customer, $sendemailStoreId);
1064 }
1065
1066 $store = $this->storeManager->getStore($customer->getStoreId());
1067
1068 $customerEmailData = $this->getFullCustomerObject($customer);
1069
1070 $this->sendEmailTemplate(
1071 $customer,
1072 $types[$type],
1073 self::XML_PATH_REGISTER_EMAIL_IDENTITY,
1074 ['customer' => $customerEmailData, 'back_url' => $backUrl, 'store' => $store],
1075 $storeId
1076 );
1077
1078 return $this;
1079 }
1080
1081 /**
1082 * Send email to customer when his password is reset
1083 *
1084 * @param CustomerInterface $customer
1085 * @return $this
1086 * @deprecated 100.1.0
1087 */
1088 protected function sendPasswordResetNotificationEmail($customer)
1089 {
1090 return $this->sendPasswordResetConfirmationEmail($customer);
1091 }
1092
1093 /**
1094 * Get either first store ID from a set website or the provided as default
1095 *
1096 * @param CustomerInterface $customer
1097 * @param int|string|null $defaultStoreId
1098 * @return int
1099 * @deprecated 100.1.0
1100 */
1101 protected function getWebsiteStoreId($customer, $defaultStoreId = null)
1102 {
1103 if ($customer->getWebsiteId() != 0 && empty($defaultStoreId)) {
1104 $storeIds = $this->storeManager->getWebsite($customer->getWebsiteId())->getStoreIds();
1105 reset($storeIds);
1106 $defaultStoreId = current($storeIds);
1107 }
1108 return $defaultStoreId;
1109 }
1110
1111 /**
1112 * @return array
1113 * @deprecated 100.1.0
1114 */
1115 protected function getTemplateTypes()
1116 {
1117 /**
1118 * self::NEW_ACCOUNT_EMAIL_REGISTERED welcome email, when confirmation is disabled
1119 * and password is set
1120 * self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD welcome email, when confirmation is disabled
1121 * and password is not set
1122 * self::NEW_ACCOUNT_EMAIL_CONFIRMED welcome email, when confirmation is enabled
1123 * and password is set
1124 * self::NEW_ACCOUNT_EMAIL_CONFIRMATION email with confirmation link
1125 */
1126 $types = [
1127 self::NEW_ACCOUNT_EMAIL_REGISTERED => self::XML_PATH_REGISTER_EMAIL_TEMPLATE,
1128 self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD => self::XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE,
1129 self::NEW_ACCOUNT_EMAIL_CONFIRMED => self::XML_PATH_CONFIRMED_EMAIL_TEMPLATE,
1130 self::NEW_ACCOUNT_EMAIL_CONFIRMATION => self::XML_PATH_CONFIRM_EMAIL_TEMPLATE,
1131 ];
1132 return $types;
1133 }
1134
1135 /**
1136 * Send corresponding email template
1137 *
1138 * @param CustomerInterface $customer
1139 * @param string $template configuration path of email template
1140 * @param string $sender configuration path of email identity
1141 * @param array $templateParams
1142 * @param int|null $storeId
1143 * @param string $email
1144 * @return $this
1145 * @deprecated 100.1.0
1146 */
1147 protected function sendEmailTemplate(
1148 $customer,
1149 $template,
1150 $sender,
1151 $templateParams = [],
1152 $storeId = null,
1153 $email = null
1154 ) {
1155 $templateId = $this->scopeConfig->getValue($template, ScopeInterface::SCOPE_STORE, $storeId);
1156 if ($email === null) {
1157 $email = $customer->getEmail();
1158 }
1159
1160 $transport = $this->transportBuilder->setTemplateIdentifier($templateId)->setTemplateOptions(
1161 ['area' => Area::AREA_FRONTEND, 'store' => $storeId]
1162 )->setTemplateVars($templateParams)->setFrom(
1163 $this->scopeConfig->getValue($sender, ScopeInterface::SCOPE_STORE, $storeId)
1164 )->addTo($email, $this->customerViewHelper->getCustomerName($customer))->getTransport();
1165
1166 $transport->sendMessage();
1167
1168 return $this;
1169 }
1170
1171 /**
1172 * Check if accounts confirmation is required in config
1173 *
1174 * @param CustomerInterface $customer
1175 * @return bool
1176 */
1177 protected function isConfirmationRequired($customer)
1178 {
1179 if ($this->canSkipConfirmation($customer)) {
1180 return false;
1181 }
1182
1183 return (bool)$this->scopeConfig->getValue(
1184 self::XML_PATH_IS_CONFIRM,
1185 ScopeInterface::SCOPE_WEBSITES,
1186 $customer->getWebsiteId()
1187 );
1188 }
1189
1190 /**
1191 * Check whether confirmation may be skipped when registering using certain email address
1192 *
1193 * @param CustomerInterface $customer
1194 * @return bool
1195 */
1196 protected function canSkipConfirmation($customer)
1197 {
1198 if (!$customer->getId()) {
1199 return false;
1200 }
1201
1202 /* If an email was used to start the registration process and it is the same email as the one
1203 used to register, then this can skip confirmation.
1204 */
1205 $skipConfirmationIfEmail = $this->registry->registry("skip_confirmation_if_email");
1206 if (!$skipConfirmationIfEmail) {
1207 return false;
1208 }
1209
1210 return strtolower($skipConfirmationIfEmail) === strtolower($customer->getEmail());
1211 }
1212
1213 /**
1214 * Check if rpToken is expired
1215 *
1216 * @param string $rpToken
1217 * @param string $rpTokenCreatedAt
1218 * @return bool
1219 */
1220 public function isResetPasswordLinkTokenExpired($rpToken, $rpTokenCreatedAt)
1221 {
1222 if (empty($rpToken) || empty($rpTokenCreatedAt)) {
1223 return true;
1224 }
1225
1226 $expirationPeriod = $this->customerModel->getResetPasswordLinkExpirationPeriod();
1227
1228 $currentTimestamp = $this->dateTimeFactory->create()->getTimestamp();
1229 $tokenTimestamp = $this->dateTimeFactory->create($rpTokenCreatedAt)->getTimestamp();
1230 if ($tokenTimestamp > $currentTimestamp) {
1231 return true;
1232 }
1233
1234 $hourDifference = floor(($currentTimestamp - $tokenTimestamp) / (60 * 60));
1235 if ($hourDifference >= $expirationPeriod) {
1236 return true;
1237 }
1238
1239 return false;
1240 }
1241
1242 /**
1243 * Change reset password link token
1244 *
1245 * Stores new reset password link token
1246 *
1247 * @param CustomerInterface $customer
1248 * @param string $passwordLinkToken
1249 * @return bool
1250 * @throws InputException
1251 */
1252 public function changeResetPasswordLinkToken($customer, $passwordLinkToken)
1253 {
1254 if (!is_string($passwordLinkToken) || empty($passwordLinkToken)) {
1255 throw new InputException(
1256 __(
1257 'Invalid value of "%value" provided for the %fieldName field.',
1258 ['value' => $passwordLinkToken, 'fieldName' => 'password reset token']
1259 )
1260 );
1261 }
1262 if (is_string($passwordLinkToken) && !empty($passwordLinkToken)) {
1263 $customerSecure = $this->customerRegistry->retrieveSecureData($customer->getId());
1264 $customerSecure->setRpToken($passwordLinkToken);
1265 $customerSecure->setRpTokenCreatedAt(
1266 $this->dateTimeFactory->create()->format(DateTime::DATETIME_PHP_FORMAT)
1267 );
1268 $this->customerRepository->save($customer);
1269 }
1270 return true;
1271 }
1272
1273 /**
1274 * Send email with new customer password
1275 *
1276 * @param CustomerInterface $customer
1277 * @return $this
1278 * @deprecated 100.1.0
1279 */
1280 public function sendPasswordReminderEmail($customer)
1281 {
1282 $storeId = $this->storeManager->getStore()->getId();
1283 if (!$storeId) {
1284 $storeId = $this->getWebsiteStoreId($customer);
1285 }
1286
1287 $customerEmailData = $this->getFullCustomerObject($customer);
1288
1289 $this->sendEmailTemplate(
1290 $customer,
1291 self::XML_PATH_REMIND_EMAIL_TEMPLATE,
1292 self::XML_PATH_FORGOT_EMAIL_IDENTITY,
1293 ['customer' => $customerEmailData, 'store' => $this->storeManager->getStore($storeId)],
1294 $storeId
1295 );
1296
1297 return $this;
1298 }
1299
1300 /**
1301 * Send email with reset password confirmation link
1302 *
1303 * @param CustomerInterface $customer
1304 * @return $this
1305 * @deprecated 100.1.0
1306 */
1307 public function sendPasswordResetConfirmationEmail($customer)
1308 {
1309 $storeId = $this->storeManager->getStore()->getId();
1310 if (!$storeId) {
1311 $storeId = $this->getWebsiteStoreId($customer);
1312 }
1313
1314 $customerEmailData = $this->getFullCustomerObject($customer);
1315
1316 $this->sendEmailTemplate(
1317 $customer,
1318 self::XML_PATH_FORGOT_EMAIL_TEMPLATE,
1319 self::XML_PATH_FORGOT_EMAIL_IDENTITY,
1320 ['customer' => $customerEmailData, 'store' => $this->storeManager->getStore($storeId)],
1321 $storeId
1322 );
1323
1324 return $this;
1325 }
1326
1327 /**
1328 * Get address by id
1329 *
1330 * @param CustomerInterface $customer
1331 * @param int $addressId
1332 * @return AddressInterface|null
1333 */
1334 protected function getAddressById(CustomerInterface $customer, $addressId)
1335 {
1336 foreach ($customer->getAddresses() as $address) {
1337 if ($address->getId() == $addressId) {
1338 return $address;
1339 }
1340 }
1341 return null;
1342 }
1343
1344 /**
1345 * Create an object with data merged from Customer and CustomerSecure
1346 *
1347 * @param CustomerInterface $customer
1348 * @return Data\CustomerSecure
1349 * @deprecated 100.1.0
1350 */
1351 protected function getFullCustomerObject($customer)
1352 {
1353 // No need to flatten the custom attributes or nested objects since the only usage is for email templates and
1354 // object passed for events
1355 $mergedCustomerData = $this->customerRegistry->retrieveSecureData($customer->getId());
1356 $customerData =
1357 $this->dataProcessor->buildOutputDataArray($customer, \Magento\Customer\Api\Data\CustomerInterface::class);
1358 $mergedCustomerData->addData($customerData);
1359 $mergedCustomerData->setData('name', $this->customerViewHelper->getCustomerName($customer));
1360 return $mergedCustomerData;
1361 }
1362
1363 /**
1364 * Return hashed password, which can be directly saved to database.
1365 *
1366 * @param string $password
1367 * @return string
1368 */
1369 public function getPasswordHash($password)
1370 {
1371 return $this->encryptor->getHash($password);
1372 }
1373
1374 /**
1375 * Get email notification
1376 *
1377 * @return EmailNotificationInterface
1378 * @deprecated 100.1.0
1379 */
1380 private function getEmailNotification()
1381 {
1382 if (!($this->emailNotification instanceof EmailNotificationInterface)) {
1383 return \Magento\Framework\App\ObjectManager::getInstance()->get(
1384 EmailNotificationInterface::class
1385 );
1386 } else {
1387 return $this->emailNotification;
1388 }
1389 }
1390
1391 /**
1392 * Destroy all active customer sessions by customer id (current session will not be destroyed).
1393 * Customer sessions which should be deleted are collecting from the "customer_visitor" table considering
1394 * configured session lifetime.
1395 *
1396 * @param string|int $customerId
1397 * @return void
1398 */
1399 private function destroyCustomerSessions($customerId)
1400 {
1401 $sessionLifetime = $this->scopeConfig->getValue(
1402 \Magento\Framework\Session\Config::XML_PATH_COOKIE_LIFETIME,
1403 \Magento\Store\Model\ScopeInterface::SCOPE_STORE
1404 );
1405 $dateTime = $this->dateTimeFactory->create();
1406 $activeSessionsTime = $dateTime->setTimestamp($dateTime->getTimestamp() - $sessionLifetime)
1407 ->format(DateTime::DATETIME_PHP_FORMAT);
1408 /** @var \Magento\Customer\Model\ResourceModel\Visitor\Collection $visitorCollection */
1409 $visitorCollection = $this->visitorCollectionFactory->create();
1410 $visitorCollection->addFieldToFilter('customer_id', $customerId);
1411 $visitorCollection->addFieldToFilter('last_visit_at', ['from' => $activeSessionsTime]);
1412 $visitorCollection->addFieldToFilter('session_id', ['neq' => $this->sessionManager->getSessionId()]);
1413 /** @var \Magento\Customer\Model\Visitor $visitor */
1414 foreach ($visitorCollection->getItems() as $visitor) {
1415 $sessionId = $visitor->getSessionId();
1416 $this->sessionManager->start();
1417 $this->saveHandler->destroy($sessionId);
1418 $this->sessionManager->writeClose();
1419 }
1420 }
1421}