diff --git a/Block/Adminhtml/Captcha/DefaultCaptcha.php b/Block/Adminhtml/Captcha/DefaultCaptcha.php new file mode 100644 index 0000000..8d6fe96 --- /dev/null +++ b/Block/Adminhtml/Captcha/DefaultCaptcha.php @@ -0,0 +1,55 @@ +_url = $url; + $this->_config = $config; + } + + /** + * Returns URL to controller action which returns new captcha image + * + * @return string + */ + public function getRefreshUrl() + { + return $this->_url->getUrl( + 'adminhtml/refresh/refresh', + ['_secure' => $this->_config->isSetFlag('web/secure/use_in_adminhtml'), '_nosecret' => true] + ); + } +} diff --git a/Block/Captcha.php b/Block/Captcha.php new file mode 100644 index 0000000..3bb7ec9 --- /dev/null +++ b/Block/Captcha.php @@ -0,0 +1,54 @@ + + */ +namespace Magento\Captcha\Block; + +/** + * @api + * @since 100.0.2 + */ +class Captcha extends \Magento\Framework\View\Element\Template +{ + /** + * Captcha data + * + * @var \Magento\Captcha\Helper\Data + */ + protected $_captchaData = null; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Captcha\Helper\Data $captchaData + * @param array $data + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Captcha\Helper\Data $captchaData, + array $data = [] + ) { + $this->_captchaData = $captchaData; + parent::__construct($context, $data); + $this->_isScopePrivate = true; + } + + /** + * Renders captcha HTML (if required) + * + * @return string + */ + protected function _toHtml() + { + $blockPath = $this->_captchaData->getCaptcha($this->getFormId())->getBlockName(); + $block = $this->getLayout()->createBlock($blockPath); + $block->setData($this->getData()); + return $block->toHtml(); + } +} diff --git a/Block/Captcha/DefaultCaptcha.php b/Block/Captcha/DefaultCaptcha.php new file mode 100644 index 0000000..027c9a9 --- /dev/null +++ b/Block/Captcha/DefaultCaptcha.php @@ -0,0 +1,86 @@ +_captchaData = $captchaData; + } + + /** + * Returns template path + * + * @return string + */ + public function getTemplate() + { + return $this->getIsAjax() ? '' : $this->_template; + } + + /** + * Returns URL to controller action which returns new captcha image + * + * @return string + */ + public function getRefreshUrl() + { + $store = $this->_storeManager->getStore(); + return $store->getUrl('captcha/refresh', ['_secure' => $store->isCurrentlySecure()]); + } + + /** + * Renders captcha HTML (if required) + * + * @return string + */ + protected function _toHtml() + { + if ($this->getCaptchaModel()->isRequired()) { + $this->getCaptchaModel()->generate(); + return parent::_toHtml(); + } + return ''; + } + + /** + * Returns captcha model + * + * @return \Magento\Captcha\Model\CaptchaInterface + */ + public function getCaptchaModel() + { + return $this->_captchaData->getCaptcha($this->getFormId()); + } +} diff --git a/Controller/Adminhtml/Refresh/Refresh.php b/Controller/Adminhtml/Refresh/Refresh.php new file mode 100644 index 0000000..cadcbdb --- /dev/null +++ b/Controller/Adminhtml/Refresh/Refresh.php @@ -0,0 +1,66 @@ +serializer = $serializer; + $this->captchaHelper = $captchaHelper; + } + + /** + * {@inheritdoc} + */ + public function execute() + { + $formId = $this->getRequest()->getPost('formId'); + $captchaModel = $this->captchaHelper->getCaptcha($formId); + $this->_view->getLayout()->createBlock( + $captchaModel->getBlockName() + )->setFormId( + $formId + )->setIsAjax( + true + )->toHtml(); + $this->getResponse()->representJson($this->serializer->serialize(['imgSrc' => $captchaModel->getImgSrc()])); + $this->_actionFlag->set('', self::FLAG_NO_POST_DISPATCH, true); + } + + /** + * Check if user has permissions to access this controller + * + * @return bool + */ + protected function _isAllowed() + { + return true; + } +} diff --git a/Controller/Refresh/Index.php b/Controller/Refresh/Index.php new file mode 100644 index 0000000..e401e03 --- /dev/null +++ b/Controller/Refresh/Index.php @@ -0,0 +1,65 @@ +captchaHelper = $captchaHelper; + $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Serialize\Serializer\Json::class); + parent::__construct($context); + } + + /** + * {@inheritdoc} + */ + public function execute() + { + $formId = $this->_request->getPost('formId'); + if (null === $formId) { + $params = []; + $content = $this->_request->getContent(); + if ($content) { + $params = $this->serializer->unserialize($content); + } + $formId = isset($params['formId']) ? $params['formId'] : null; + } + $captchaModel = $this->captchaHelper->getCaptcha($formId); + $captchaModel->generate(); + + $block = $this->_view->getLayout()->createBlock($captchaModel->getBlockName()); + $block->setFormId($formId)->setIsAjax(true)->toHtml(); + $this->_response->representJson($this->serializer->serialize(['imgSrc' => $captchaModel->getImgSrc()])); + $this->_actionFlag->set('', self::FLAG_NO_POST_DISPATCH, true); + } +} diff --git a/Cron/DeleteExpiredImages.php b/Cron/DeleteExpiredImages.php new file mode 100644 index 0000000..cb5f1c4 --- /dev/null +++ b/Cron/DeleteExpiredImages.php @@ -0,0 +1,94 @@ +_helper = $helper; + $this->_adminHelper = $adminHelper; + $this->_mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->_storeManager = $storeManager; + } + + /** + * Delete Expired Captcha Images + * + * @return \Magento\Captcha\Cron\DeleteExpiredImages + */ + public function execute() + { + foreach ($this->_storeManager->getWebsites() as $website) { + $this->_deleteExpiredImagesForWebsite($this->_helper, $website, $website->getDefaultStore()); + } + $this->_deleteExpiredImagesForWebsite($this->_adminHelper); + + return $this; + } + + /** + * Delete Expired Captcha Images for specific website + * + * @param \Magento\Captcha\Helper\Data $helper + * @param \Magento\Store\Model\Website|null $website + * @param \Magento\Store\Model\Store|null $store + * @return void + */ + protected function _deleteExpiredImagesForWebsite( + \Magento\Captcha\Helper\Data $helper, + \Magento\Store\Model\Website $website = null, + \Magento\Store\Model\Store $store = null + ) { + $expire = time() - $helper->getConfig('timeout', $store) * 60; + $imageDirectory = $this->_mediaDirectory->getRelativePath($helper->getImgDir($website)); + foreach ($this->_mediaDirectory->read($imageDirectory) as $filePath) { + if ($this->_mediaDirectory->isFile($filePath) + && pathinfo($filePath, PATHINFO_EXTENSION) == 'png' + && $this->_mediaDirectory->stat($filePath)['mtime'] < $expire + ) { + $this->_mediaDirectory->delete($filePath); + } + } + } +} diff --git a/Cron/DeleteOldAttempts.php b/Cron/DeleteOldAttempts.php new file mode 100644 index 0000000..0e63512 --- /dev/null +++ b/Cron/DeleteOldAttempts.php @@ -0,0 +1,38 @@ +resLogFactory = $resLogFactory; + } + + /** + * Delete Unnecessary logged attempts + * + * @return \Magento\Captcha\Cron\DeleteOldAttempts + */ + public function execute() + { + $this->resLogFactory->create()->deleteOldAttempts(); + + return $this; + } +} diff --git a/CustomerData/Captcha.php b/CustomerData/Captcha.php new file mode 100644 index 0000000..e07bf95 --- /dev/null +++ b/CustomerData/Captcha.php @@ -0,0 +1,81 @@ +helper = $helper; + $this->formIds = $formIds; + parent::__construct($data); + $this->customerSession = $customerSession ?? ObjectManager::getInstance()->get(CustomerSession::class); + } + + /** + * @inheritdoc + */ + public function getSectionData() :array + { + $data = []; + + foreach ($this->formIds as $formId) { + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->helper->getCaptcha($formId); + $login = ''; + if ($this->customerSession->isLoggedIn()) { + $login = $this->customerSession->getCustomerData()->getEmail(); + } + $required = $captchaModel->isRequired($login); + $data[$formId] = [ + 'isRequired' => $required, + 'timestamp' => time() + ]; + } + + return $data; + } +} diff --git a/Helper/Adminhtml/Data.php b/Helper/Adminhtml/Data.php new file mode 100644 index 0000000..adf8b7d --- /dev/null +++ b/Helper/Adminhtml/Data.php @@ -0,0 +1,61 @@ + + */ +namespace Magento\Captcha\Helper\Adminhtml; + +class Data extends \Magento\Captcha\Helper\Data +{ + /** + * @var \Magento\Backend\App\ConfigInterface + */ + protected $_backendConfig; + + /** + * @param \Magento\Framework\App\Helper\Context $context + * @param \Magento\Store\Model\StoreManager $storeManager + * @param \Magento\Framework\Filesystem $filesystem + * @param \Magento\Captcha\Model\CaptchaFactory $factory + * @param \Magento\Backend\App\ConfigInterface $backendConfig + */ + public function __construct( + \Magento\Framework\App\Helper\Context $context, + \Magento\Store\Model\StoreManager $storeManager, + \Magento\Framework\Filesystem $filesystem, + \Magento\Captcha\Model\CaptchaFactory $factory, + \Magento\Backend\App\ConfigInterface $backendConfig + ) { + $this->_backendConfig = $backendConfig; + parent::__construct($context, $storeManager, $filesystem, $factory); + } + + /** + * Returns config value for admin captcha + * + * @param string $key The last part of XML_PATH_$area_CAPTCHA_ constant (case insensitive) + * @param \Magento\Store\Model\Store $store + * @return \Magento\Framework\App\Config\Element + */ + public function getConfig($key, $store = null) + { + return $this->_backendConfig->getValue('admin/captcha/' . $key); + } + + /** + * Get website code + * + * @param mixed $website + * @return string + */ + protected function _getWebsiteCode($website = null) + { + return 'admin'; + } +} diff --git a/Helper/Data.php b/Helper/Data.php new file mode 100644 index 0000000..9a14f9a --- /dev/null +++ b/Helper/Data.php @@ -0,0 +1,184 @@ +_storeManager = $storeManager; + $this->_filesystem = $filesystem; + $this->_factory = $factory; + parent::__construct($context); + } + + /** + * Get Captcha + * + * @param string $formId + * @return \Magento\Captcha\Model\CaptchaInterface + */ + public function getCaptcha($formId) + { + if (!array_key_exists($formId, $this->_captcha)) { + $captchaType = ucfirst($this->getConfig('type')); + if (!$captchaType) { + $captchaType = self::DEFAULT_CAPTCHA_TYPE; + } elseif ($captchaType == 'Default') { + $captchaType = $captchaType . 'Model'; + } + + $this->_captcha[$formId] = $this->_factory->create($captchaType, $formId); + } + return $this->_captcha[$formId]; + } + + /** + * Returns config value + * + * @param string $key The last part of XML_PATH_$area_CAPTCHA_ constant (case insensitive) + * @param \Magento\Store\Model\Store $store + * @return \Magento\Framework\App\Config\Element + */ + public function getConfig($key, $store = null) + { + return $this->scopeConfig->getValue( + 'customer/captcha/' . $key, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $store + ); + } + + /** + * Get list of available fonts. + * + * Return format: + * [['arial'] => ['label' => 'Arial', 'path' => '/www/magento/fonts/arial.ttf']] + * + * @return array + */ + public function getFonts() + { + $fontsConfig = $this->scopeConfig->getValue(\Magento\Captcha\Helper\Data::XML_PATH_CAPTCHA_FONTS, 'default'); + $fonts = []; + if ($fontsConfig) { + $libDir = $this->_filesystem->getDirectoryRead(DirectoryList::LIB_INTERNAL); + foreach ($fontsConfig as $fontName => $fontConfig) { + $fonts[$fontName] = [ + 'label' => $fontConfig['label'], + 'path' => $libDir->getAbsolutePath($fontConfig['path']), + ]; + } + } + return $fonts; + } + + /** + * Get captcha image directory + * + * @param mixed $website + * @return string + */ + public function getImgDir($website = null) + { + $mediaDir = $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $captchaDir = '/captcha/' . $this->_getWebsiteCode($website); + $mediaDir->create($captchaDir); + return $mediaDir->getAbsolutePath($captchaDir) . '/'; + } + + /** + * Get website code + * + * @param mixed $website + * @return string + */ + protected function _getWebsiteCode($website = null) + { + return $this->_storeManager->getWebsite($website)->getCode(); + } + + /** + * Get captcha image base URL + * + * @param mixed $website + * @return string + */ + public function getImgUrl($website = null) + { + return $this->_storeManager->getStore()->getBaseUrl( + DirectoryList::MEDIA + ) . 'captcha' . '/' . $this->_getWebsiteCode( + $website + ) . '/'; + } +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..49525fd --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/LICENSE_AFL.txt b/LICENSE_AFL.txt new file mode 100644 index 0000000..f39d641 --- /dev/null +++ b/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/Model/CaptchaFactory.php b/Model/CaptchaFactory.php new file mode 100644 index 0000000..c1a8342 --- /dev/null +++ b/Model/CaptchaFactory.php @@ -0,0 +1,45 @@ +_objectManager = $objectManager; + } + + /** + * Get captcha instance + * + * @param string $captchaType + * @param string $formId + * @return \Magento\Captcha\Model\CaptchaInterface + * @throws \InvalidArgumentException + */ + public function create($captchaType, $formId) + { + $className = 'Magento\Captcha\Model\\' . ucfirst($captchaType); + + $instance = $this->_objectManager->create($className, ['formId' => $formId]); + if (!$instance instanceof \Magento\Captcha\Model\CaptchaInterface) { + throw new \InvalidArgumentException( + $className . ' does not implement \Magento\Captcha\Model\CaptchaInterface' + ); + } + return $instance; + } +} diff --git a/Model/CaptchaInterface.php b/Model/CaptchaInterface.php new file mode 100644 index 0000000..3b5d5ee --- /dev/null +++ b/Model/CaptchaInterface.php @@ -0,0 +1,40 @@ +configProvider = $configProvider; + } + + /** + * @param \Magento\Checkout\Block\Cart\Sidebar $subject + * @param array $result + * @return array + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetConfig(\Magento\Checkout\Block\Cart\Sidebar $subject, array $result) + { + return array_merge_recursive($result, $this->configProvider->getConfig()); + } +} diff --git a/Model/Checkout/ConfigProvider.php b/Model/Checkout/ConfigProvider.php new file mode 100644 index 0000000..34ee620 --- /dev/null +++ b/Model/Checkout/ConfigProvider.php @@ -0,0 +1,132 @@ +storeManager = $storeManager; + $this->captchaData = $captchaData; + $this->formIds = $formIds; + } + + /** + * @inheritdoc + */ + public function getConfig() + { + $config = []; + foreach ($this->formIds as $formId) { + $config['captcha'][$formId] = [ + 'isCaseSensitive' => $this->isCaseSensitive($formId), + 'imageHeight' => $this->getImageHeight($formId), + 'imageSrc' => $this->getImageSrc($formId), + 'refreshUrl' => $this->getRefreshUrl(), + 'isRequired' => $this->isRequired($formId), + 'timestamp' => time() + ]; + } + return $config; + } + + /** + * Returns is captcha case sensitive + * + * @param string $formId + * @return bool + */ + protected function isCaseSensitive($formId) + { + return (boolean)$this->getCaptchaModel($formId)->isCaseSensitive(); + } + + /** + * Returns captcha image height + * + * @param string $formId + * @return int + */ + protected function getImageHeight($formId) + { + return $this->getCaptchaModel($formId)->getHeight(); + } + + /** + * Returns captcha image source path + * + * @param string $formId + * @return string + */ + protected function getImageSrc($formId) + { + if ($this->isRequired($formId)) { + $captcha = $this->getCaptchaModel($formId); + $captcha->generate(); + return $captcha->getImgSrc(); + } + return ''; + } + + /** + * Returns URL to controller action which returns new captcha image + * + * @return string + */ + protected function getRefreshUrl() + { + $store = $this->storeManager->getStore(); + return $store->getUrl('captcha/refresh', ['_secure' => $store->isCurrentlySecure()]); + } + + /** + * Whether captcha is required to be inserted to this form + * + * @param string $formId + * @return bool + */ + protected function isRequired($formId) + { + return (boolean)$this->getCaptchaModel($formId)->isRequired(); + } + + /** + * Return captcha model for specified form + * + * @param string $formId + * @return \Magento\Captcha\Model\CaptchaInterface + */ + protected function getCaptchaModel($formId) + { + return $this->captchaData->getCaptcha($formId); + } +} diff --git a/Model/Config/Font.php b/Model/Config/Font.php new file mode 100644 index 0000000..cce57e6 --- /dev/null +++ b/Model/Config/Font.php @@ -0,0 +1,44 @@ + + */ +namespace Magento\Captcha\Model\Config; + +class Font implements \Magento\Framework\Option\ArrayInterface +{ + /** + * Captcha data + * + * @var \Magento\Captcha\Helper\Data + */ + protected $_captchaData = null; + + /** + * @param \Magento\Captcha\Helper\Data $captchaData + */ + public function __construct(\Magento\Captcha\Helper\Data $captchaData) + { + $this->_captchaData = $captchaData; + } + + /** + * Get options for font selection field + * + * @return array + */ + public function toOptionArray() + { + $optionArray = []; + foreach ($this->_captchaData->getFonts() as $fontName => $fontData) { + $optionArray[] = ['label' => $fontData['label'], 'value' => $fontName]; + } + return $optionArray; + } +} diff --git a/Model/Config/Form/AbstractForm.php b/Model/Config/Form/AbstractForm.php new file mode 100644 index 0000000..811e455 --- /dev/null +++ b/Model/Config/Form/AbstractForm.php @@ -0,0 +1,41 @@ + + */ +namespace Magento\Captcha\Model\Config\Form; + +use Magento\Framework\App\Config\Value; + +abstract class AbstractForm extends Value implements \Magento\Framework\Option\ArrayInterface +{ + /** + * @var string + */ + protected $_configPath; + + /** + * Returns options for form multiselect + * + * @return array + */ + public function toOptionArray() + { + $optionArray = []; + $backendConfig = $this->_config->getValue($this->_configPath, 'default'); + if ($backendConfig) { + foreach ($backendConfig as $formName => $formConfig) { + if (!empty($formConfig['label'])) { + $optionArray[] = ['label' => $formConfig['label'], 'value' => $formName]; + } + } + } + return $optionArray; + } +} diff --git a/Model/Config/Form/Backend.php b/Model/Config/Form/Backend.php new file mode 100644 index 0000000..9fad494 --- /dev/null +++ b/Model/Config/Form/Backend.php @@ -0,0 +1,20 @@ + + */ +namespace Magento\Captcha\Model\Config\Form; + +class Backend extends \Magento\Captcha\Model\Config\Form\AbstractForm +{ + /** + * @var string + */ + protected $_configPath = 'captcha/backend/areas'; +} diff --git a/Model/Config/Form/Frontend.php b/Model/Config/Form/Frontend.php new file mode 100644 index 0000000..84c1a00 --- /dev/null +++ b/Model/Config/Form/Frontend.php @@ -0,0 +1,20 @@ + + */ +namespace Magento\Captcha\Model\Config\Form; + +class Frontend extends \Magento\Captcha\Model\Config\Form\AbstractForm +{ + /** + * @var string + */ + protected $_configPath = 'captcha/frontend/areas'; +} diff --git a/Model/Config/Mode.php b/Model/Config/Mode.php new file mode 100644 index 0000000..b1f0239 --- /dev/null +++ b/Model/Config/Mode.php @@ -0,0 +1,31 @@ + + */ +namespace Magento\Captcha\Model\Config; + +class Mode implements \Magento\Framework\Option\ArrayInterface +{ + /** + * Get options for captcha mode selection field + * + * @return array + */ + public function toOptionArray() + { + return [ + ['label' => __('Always'), 'value' => \Magento\Captcha\Helper\Data::MODE_ALWAYS], + [ + 'label' => __('After number of attempts to login'), + 'value' => \Magento\Captcha\Helper\Data::MODE_AFTER_FAIL + ] + ]; + } +} diff --git a/Model/Customer/Plugin/AjaxLogin.php b/Model/Customer/Plugin/AjaxLogin.php new file mode 100644 index 0000000..84ac710 --- /dev/null +++ b/Model/Customer/Plugin/AjaxLogin.php @@ -0,0 +1,125 @@ +helper = $helper; + $this->sessionManager = $sessionManager; + $this->resultJsonFactory = $resultJsonFactory; + $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->formIds = $formIds; + } + + /** + * Check captcha data on login action. + * + * @param \Magento\Customer\Controller\Ajax\Login $subject + * @param \Closure $proceed + * @return $this + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function aroundExecute( + \Magento\Customer\Controller\Ajax\Login $subject, + \Closure $proceed + ) { + $captchaFormIdField = 'captcha_form_id'; + $captchaInputName = 'captcha_string'; + + /** @var \Magento\Framework\App\RequestInterface $request */ + $request = $subject->getRequest(); + + $loginParams = []; + $content = $request->getContent(); + if ($content) { + $loginParams = $this->serializer->unserialize($content); + } + $username = $loginParams['username'] ?? null; + $captchaString = $loginParams[$captchaInputName] ?? null; + $loginFormId = $loginParams[$captchaFormIdField] ?? null; + + if (!in_array($loginFormId, $this->formIds) && $this->helper->getCaptcha($loginFormId)->isRequired($username)) { + return $this->returnJsonError(__('Provided form does not exist')); + } + + foreach ($this->formIds as $formId) { + if ($formId === $loginFormId) { + $captchaModel = $this->helper->getCaptcha($formId); + if ($captchaModel->isRequired($username)) { + if (!$captchaModel->isCorrect($captchaString)) { + $this->sessionManager->setUsername($username); + $captchaModel->logAttempt($username); + return $this->returnJsonError(__('Incorrect CAPTCHA')); + } + } + $captchaModel->logAttempt($username); + } + } + return $proceed(); + } + + /** + * Format JSON response. + * + * @param \Magento\Framework\Phrase $phrase + * @return \Magento\Framework\Controller\Result\Json + */ + private function returnJsonError(\Magento\Framework\Phrase $phrase): \Magento\Framework\Controller\Result\Json + { + $resultJson = $this->resultJsonFactory->create(); + return $resultJson->setData(['errors' => true, 'message' => $phrase]); + } +} diff --git a/Model/DefaultModel.php b/Model/DefaultModel.php new file mode 100644 index 0000000..bbbbfb0 --- /dev/null +++ b/Model/DefaultModel.php @@ -0,0 +1,576 @@ +session = $session; + $this->captchaData = $captchaData; + $this->resLogFactory = $resLogFactory; + $this->formId = $formId; + $this->randomMath = $randomMath ?? \Magento\Framework\App\ObjectManager::getInstance()->get(Random::class); + } + + /** + * Returns key with respect of current form ID + * + * @param string $key + * @return string + */ + private function getFormIdKey($key) + { + return $this->formId . '_' . $key; + } + + /** + * Get Block Name + * + * @return string + */ + public function getBlockName() + { + return \Magento\Captcha\Block\Captcha\DefaultCaptcha::class; + } + + /** + * Whether captcha is required to be inserted to this form + * + * @param null|string $login + * @return bool + */ + public function isRequired($login = null) + { + if (($this->isUserAuth() + && !$this->isShownToLoggedInUser()) + || !$this->isEnabled() + || !in_array( + $this->formId, + $this->getTargetForms() + ) + ) { + return false; + } + + return $this->isShowAlways() + || $this->isOverLimitAttempts($login) + || $this->session->getData($this->getFormIdKey('show_captcha')); + } + + /** + * Check if CAPTCHA has to be shown to logged in user on this form + * + * @return bool + */ + public function isShownToLoggedInUser() + { + $forms = (array)$this->captchaData->getConfig('shown_to_logged_in_user'); + foreach ($forms as $formId => $isShownToLoggedIn) { + if ($isShownToLoggedIn && $this->formId == $formId) { + return true; + } + } + return false; + } + + /** + * Check is over limit attempts + * + * @param string $login + * @return bool + */ + private function isOverLimitAttempts($login) + { + return $this->isOverLimitIpAttempt() || $this->isOverLimitLoginAttempts($login); + } + + /** + * Returns number of allowed attempts for same login + * + * @return int + */ + private function getAllowedAttemptsForSameLogin() + { + return (int)$this->captchaData->getConfig('failed_attempts_login'); + } + + /** + * Returns number of allowed attempts from same IP + * + * @return int + */ + private function getAllowedAttemptsFromSameIp() + { + return (int)$this->captchaData->getConfig('failed_attempts_ip'); + } + + /** + * Check is over limit saved attempts from one ip + * + * @return bool + */ + private function isOverLimitIpAttempt() + { + $countAttemptsByIp = $this->getResourceModel()->countAttemptsByRemoteAddress(); + return $countAttemptsByIp >= $this->getAllowedAttemptsFromSameIp(); + } + + /** + * Is Over Limit Login Attempts + * + * @param string $login + * @return bool + */ + private function isOverLimitLoginAttempts($login) + { + if ($login != false) { + $countAttemptsByLogin = $this->getResourceModel()->countAttemptsByUserLogin($login); + return $countAttemptsByLogin >= $this->getAllowedAttemptsForSameLogin(); + } + return false; + } + + /** + * Check is user auth + * + * @return bool + */ + private function isUserAuth() + { + return $this->session->isLoggedIn(); + } + + /** + * Whether to respect case while checking the answer + * + * @return bool + */ + public function isCaseSensitive() + { + return (string)$this->captchaData->getConfig('case_sensitive'); + } + + /** + * Get font to use when generating captcha + * + * @return string + */ + public function getFont() + { + $font = (string)$this->captchaData->getConfig('font'); + $fonts = $this->captchaData->getFonts(); + + if (isset($fonts[$font])) { + $fontPath = $fonts[$font]['path']; + } else { + $fontData = array_shift($fonts); + $fontPath = $fontData['path']; + } + + return $fontPath; + } + + /** + * After this time isCorrect() is going to return FALSE even if word was guessed correctly + * + * @return int + */ + public function getExpiration() + { + if (!$this->expiration) { + /** + * as "timeout" configuration parameter specifies timeout in minutes - we multiply it on 60 to set + * expiration in seconds + */ + $this->expiration = (int)$this->captchaData->getConfig('timeout') * 60; + } + return $this->expiration; + } + + /** + * Get timeout for session token + * + * @return int + */ + public function getTimeout() + { + return $this->getExpiration(); + } + + /** + * Get captcha image directory + * + * @return string + */ + public function getImgDir() + { + return $this->captchaData->getImgDir(); + } + + /** + * Get captcha image base URL + * + * @return string + */ + public function getImgUrl() + { + return $this->captchaData->getImgUrl(); + } + + /** + * Checks whether captcha was guessed correctly by user + * + * @param string $word + * @return bool + */ + public function isCorrect($word) + { + $storedWords = $this->getWords(); + $this->clearWord(); + + if (!$word || !$storedWords) { + return false; + } + + if (!$this->isCaseSensitive()) { + $storedWords = strtolower($storedWords); + $word = strtolower($word); + } + return in_array($word, explode(',', $storedWords)); + } + + /** + * Return full URL to captcha image + * + * @return string + */ + public function getImgSrc() + { + return $this->getImgUrl() . $this->getId() . $this->getSuffix(); + } + + /** + * Log attempt + * + * @param string $login + * @return $this + */ + public function logAttempt($login) + { + if ($this->isEnabled() && in_array($this->formId, $this->getTargetForms())) { + $this->getResourceModel()->logAttempt($login); + if ($this->isOverLimitLoginAttempts($login)) { + $this->setShowCaptchaInSession(true); + } + } + return $this; + } + + /** + * Set show_captcha flag in session + * + * @param bool $value + * @return void + * @since 100.1.0 + */ + public function setShowCaptchaInSession($value = true) + { + if ($value !== true) { + $value = false; + } + + $this->session->setData($this->getFormIdKey('show_captcha'), $value); + } + + /** + * Generate word used for captcha render + * + * @return string + * @throws \Magento\Framework\Exception\LocalizedException + * @since 100.2.0 + */ + protected function generateWord() + { + $symbols = (string)$this->captchaData->getConfig('symbols'); + $wordLen = $this->getWordLen(); + return $this->randomMath->getRandomString($wordLen, $symbols); + } + + /** + * Returns length for generating captcha word. This value may be dynamic. + * + * @return int + * @throws \Magento\Framework\Exception\LocalizedException + * @since 100.2.0 + */ + public function getWordLen() + { + $from = 0; + $to = 0; + $length = (string)$this->captchaData->getConfig('length'); + if (!is_numeric($length)) { + if (preg_match('/(\d+)-(\d+)/', $length, $matches)) { + $from = (int)$matches[1]; + $to = (int)$matches[2]; + } + } else { + $from = (int)$length; + $to = (int)$length; + } + + if ($to < $from || $from < 1 || $to < 1) { + $from = self::DEFAULT_WORD_LENGTH_FROM; + $to = self::DEFAULT_WORD_LENGTH_TO; + } + + return \Magento\Framework\Math\Random::getRandomNumber($from, $to); + } + + /** + * Whether to show captcha for this form every time + * + * @return bool + */ + private function isShowAlways() + { + $captchaMode = (string)$this->captchaData->getConfig('mode'); + + if ($captchaMode === Data::MODE_ALWAYS) { + return true; + } + + if ($captchaMode === Data::MODE_AFTER_FAIL + && $this->getAllowedAttemptsForSameLogin() === 0 + ) { + return true; + } + + $alwaysFor = $this->captchaData->getConfig('always_for'); + foreach ($alwaysFor as $nodeFormId => $isAlwaysFor) { + if ($isAlwaysFor && $this->formId == $nodeFormId) { + return true; + } + } + + return false; + } + + /** + * Whether captcha is enabled at this area + * + * @return bool + */ + private function isEnabled() + { + return (string)$this->captchaData->getConfig('enable'); + } + + /** + * Retrieve list of forms where captcha must be shown + * + * For frontend this list is based on current website + * + * @return array + */ + private function getTargetForms() + { + $formsString = (string)$this->captchaData->getConfig('forms'); + return explode(',', $formsString); + } + + /** + * Get captcha word + * + * @return string|null + */ + public function getWord() + { + $sessionData = $this->session->getData($this->getFormIdKey(self::SESSION_WORD)); + return time() < $sessionData['expires'] ? $sessionData['data'] : null; + } + + /** + * Get captcha words + * + * @return string|null + */ + private function getWords() + { + $sessionData = $this->session->getData($this->getFormIdKey(self::SESSION_WORD)); + return time() < $sessionData['expires'] ? $sessionData['words'] : null; + } + + /** + * Set captcha word + * + * @param string $word + * @return $this + * @since 100.2.0 + */ + protected function setWord($word) + { + $this->words = $this->words ? $this->words . ',' . $word : $word; + $this->session->setData( + $this->getFormIdKey(self::SESSION_WORD), + ['data' => $word, 'words' => $this->words, 'expires' => time() + $this->getTimeout()] + ); + $this->word = $word; + return $this; + } + + /** + * Set captcha word + * + * @return $this + */ + private function clearWord() + { + $this->session->unsetData($this->getFormIdKey(self::SESSION_WORD)); + $this->word = null; + return $this; + } + + /** + * Override function to generate less curly captcha that will not cut off + * + * @see \Zend\Captcha\Image::_randomSize() + * @return int + * @throws \Magento\Framework\Exception\LocalizedException + * @since 100.2.0 + */ + protected function randomSize() + { + return \Magento\Framework\Math\Random::getRandomNumber(280, 300) / 100; + } + + /** + * Overlap of the parent method + * + * @return void + * + * Now deleting old captcha images make crontab script + * @see \Magento\Captcha\Cron\DeleteExpiredImages::execute + * + * Added SuppressWarnings since this method is declared in parent class and we can not use other method name. + * @SuppressWarnings(PHPMD.ShortMethodName) + * @since 100.2.0 + */ + protected function gc() + { + return; // required for static testing to pass + } + + /** + * Get resource model + * + * @return \Magento\Captcha\Model\ResourceModel\Log + */ + private function getResourceModel() + { + return $this->resLogFactory->create(); + } +} diff --git a/Model/ResourceModel/Log.php b/Model/ResourceModel/Log.php new file mode 100644 index 0000000..95b7d3a --- /dev/null +++ b/Model/ResourceModel/Log.php @@ -0,0 +1,192 @@ + + */ +class Log extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +{ + /** + * Type Remote Address + */ + const TYPE_REMOTE_ADDRESS = 1; + + /** + * Type User Login Name + */ + const TYPE_LOGIN = 2; + + /** + * Core Date + * + * @var \Magento\Framework\Stdlib\DateTime\DateTime + */ + protected $_coreDate; + + /** + * @var \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress + */ + protected $_remoteAddress; + + /** + * @param \Magento\Framework\Model\ResourceModel\Db\Context $context + * @param \Magento\Framework\Stdlib\DateTime\DateTime $coreDate + * @param \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress $remoteAddress + * @param string $connectionName + */ + public function __construct( + \Magento\Framework\Model\ResourceModel\Db\Context $context, + \Magento\Framework\Stdlib\DateTime\DateTime $coreDate, + \Magento\Framework\HTTP\PhpEnvironment\RemoteAddress $remoteAddress, + $connectionName = null + ) { + $this->_coreDate = $coreDate; + $this->_remoteAddress = $remoteAddress; + parent::__construct($context, $connectionName); + } + + /** + * Define main table + * + * @return void + */ + protected function _construct() + { + $this->_setMainTable('captcha_log'); + } + + /** + * Save or Update count Attempts + * + * @param string|null $login + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function logAttempt($login) + { + if ($login != null) { + $this->getConnection()->insertOnDuplicate( + $this->getMainTable(), + [ + 'type' => self::TYPE_LOGIN, + 'value' => $login, + 'count' => 1, + 'updated_at' => $this->_coreDate->gmtDate() + ], + ['count' => new \Zend\Db\Sql\Expression('count+1'), 'updated_at'] + ); + } + $ip = $this->_remoteAddress->getRemoteAddress(); + if ($ip != null) { + $this->getConnection()->insertOnDuplicate( + $this->getMainTable(), + [ + 'type' => self::TYPE_REMOTE_ADDRESS, + 'value' => $ip, + 'count' => 1, + 'updated_at' => $this->_coreDate->gmtDate() + ], + ['count' => new \Zend\Db\Sql\Expression('count+1'), 'updated_at'] + ); + } + return $this; + } + + /** + * Delete User attempts by login + * + * @param string $login + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function deleteUserAttempts($login) + { + if ($login != null) { + $this->getConnection()->delete( + $this->getMainTable(), + ['type = ?' => self::TYPE_LOGIN, 'value = ?' => $login] + ); + } + $ip = $this->_remoteAddress->getRemoteAddress(); + if ($ip != null) { + $this->getConnection()->delete( + $this->getMainTable(), + ['type = ?' => self::TYPE_REMOTE_ADDRESS, 'value = ?' => $ip] + ); + } + + return $this; + } + + /** + * Get count attempts by ip + * + * @return null|int + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function countAttemptsByRemoteAddress() + { + $ip = $this->_remoteAddress->getRemoteAddress(); + if (!$ip) { + return 0; + } + $connection = $this->getConnection(); + $select = $connection->select()->from( + $this->getMainTable(), + 'count' + )->where( + 'type = ?', + self::TYPE_REMOTE_ADDRESS + )->where( + 'value = ?', + $ip + ); + return $connection->fetchOne($select); + } + + /** + * Get count attempts by user login + * + * @param string $login + * @return null|int + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function countAttemptsByUserLogin($login) + { + if (!$login) { + return 0; + } + $connection = $this->getConnection(); + $select = $connection->select()->from( + $this->getMainTable(), + 'count' + )->where( + 'type = ?', + self::TYPE_LOGIN + )->where( + 'value = ?', + $login + ); + return $connection->fetchOne($select); + } + + /** + * Delete attempts with expired in update_at time + * + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function deleteOldAttempts() + { + $this->getConnection()->delete( + $this->getMainTable(), + ['updated_at < ?' => $this->_coreDate->gmtDate(null, time() - 60 * 30)] + ); + } +} diff --git a/Observer/CaptchaStringResolver.php b/Observer/CaptchaStringResolver.php new file mode 100644 index 0000000..d83abc7 --- /dev/null +++ b/Observer/CaptchaStringResolver.php @@ -0,0 +1,35 @@ +getPost(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE); + if (!empty($captchaParams) && !empty($captchaParams[$formId])) { + $value = $captchaParams[$formId]; + } else { + //For Web APIs + $value = $request->getHeader('X-Captcha'); + } + + return $value; + } +} diff --git a/Observer/CheckContactUsFormObserver.php b/Observer/CheckContactUsFormObserver.php new file mode 100644 index 0000000..8c1da0e --- /dev/null +++ b/Observer/CheckContactUsFormObserver.php @@ -0,0 +1,104 @@ +_helper = $helper; + $this->_actionFlag = $actionFlag; + $this->messageManager = $messageManager; + $this->redirect = $redirect; + $this->captchaStringResolver = $captchaStringResolver; + } + + /** + * Check CAPTCHA on Contact Us page + * + * @param \Magento\Framework\Event\Observer $observer + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $formId = 'contact_us'; + $captcha = $this->_helper->getCaptcha($formId); + if ($captcha->isRequired()) { + /** @var \Magento\Framework\App\Action\Action $controller */ + $controller = $observer->getControllerAction(); + if (!$captcha->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA.')); + $this->getDataPersistor()->set($formId, $controller->getRequest()->getPostValue()); + $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); + $this->redirect->redirect($controller->getResponse(), 'contact/index/index'); + } + } + } + + /** + * Get Data Persistor + * + * @return DataPersistorInterface + */ + private function getDataPersistor() + { + if ($this->dataPersistor === null) { + $this->dataPersistor = ObjectManager::getInstance() + ->get(DataPersistorInterface::class); + } + + return $this->dataPersistor; + } +} diff --git a/Observer/CheckForgotpasswordObserver.php b/Observer/CheckForgotpasswordObserver.php new file mode 100644 index 0000000..623d119 --- /dev/null +++ b/Observer/CheckForgotpasswordObserver.php @@ -0,0 +1,83 @@ +_helper = $helper; + $this->_actionFlag = $actionFlag; + $this->messageManager = $messageManager; + $this->redirect = $redirect; + $this->captchaStringResolver = $captchaStringResolver; + } + + /** + * Check Captcha On Forgot Password Page + * + * @param \Magento\Framework\Event\Observer $observer + * @return $this + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $formId = 'user_forgotpassword'; + $captchaModel = $this->_helper->getCaptcha($formId); + if ($captchaModel->isRequired()) { + /** @var \Magento\Framework\App\Action\Action $controller */ + $controller = $observer->getControllerAction(); + if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); + $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); + $this->redirect->redirect($controller->getResponse(), '*/*/forgotpassword'); + } + } + + return $this; + } +} diff --git a/Observer/CheckUserCreateObserver.php b/Observer/CheckUserCreateObserver.php new file mode 100644 index 0000000..ef66116 --- /dev/null +++ b/Observer/CheckUserCreateObserver.php @@ -0,0 +1,104 @@ +_helper = $helper; + $this->_actionFlag = $actionFlag; + $this->messageManager = $messageManager; + $this->_session = $session; + $this->_urlManager = $urlManager; + $this->redirect = $redirect; + $this->captchaStringResolver = $captchaStringResolver; + } + + /** + * Check Captcha On User Login Page + * + * @param \Magento\Framework\Event\Observer $observer + * @return $this + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $formId = 'user_create'; + $captchaModel = $this->_helper->getCaptcha($formId); + if ($captchaModel->isRequired()) { + /** @var \Magento\Framework\App\Action\Action $controller */ + $controller = $observer->getControllerAction(); + if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) { + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); + $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); + $this->_session->setCustomerFormData($controller->getRequest()->getPostValue()); + $url = $this->_urlManager->getUrl('*/*/create', ['_nosecret' => true]); + $controller->getResponse()->setRedirect($this->redirect->error($url)); + } + } + + return $this; + } +} diff --git a/Observer/CheckUserEditObserver.php b/Observer/CheckUserEditObserver.php new file mode 100644 index 0000000..872bbec --- /dev/null +++ b/Observer/CheckUserEditObserver.php @@ -0,0 +1,136 @@ +helper = $helper; + $this->actionFlag = $actionFlag; + $this->messageManager = $messageManager; + $this->redirect = $redirect; + $this->captchaStringResolver = $captchaStringResolver; + $this->authentication = $authentication; + $this->customerSession = $customerSession; + $this->scopeConfig = $scopeConfig; + } + + /** + * Check Captcha On Forgot Password Page + * + * @param \Magento\Framework\Event\Observer $observer + * @return $this|void + * @throws \Magento\Framework\Exception\SessionException + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $captchaModel = $this->helper->getCaptcha(self::FORM_ID); + if ($captchaModel->isRequired()) { + /** @var \Magento\Framework\App\Action\Action $controller */ + $controller = $observer->getControllerAction(); + if (!$captchaModel->isCorrect( + $this->captchaStringResolver->resolve( + $controller->getRequest(), + self::FORM_ID + ) + )) { + $customerId = $this->customerSession->getCustomerId(); + $this->authentication->processAuthenticationFailure($customerId); + if ($this->authentication->isLocked($customerId)) { + $this->customerSession->logout(); + $this->customerSession->start(); + $message = __( + 'The account is locked. Please wait and try again or contact %1.', + $this->scopeConfig->getValue('contact/email/recipient_email') + ); + $this->messageManager->addErrorMessage($message); + } + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); + $this->actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); + $this->redirect->redirect($controller->getResponse(), '*/*/edit'); + } + } + + $customer = $this->customerSession->getCustomer(); + $login = $customer->getEmail(); + $captchaModel->logAttempt($login); + + return $this; + } +} diff --git a/Observer/CheckUserForgotPasswordBackendObserver.php b/Observer/CheckUserForgotPasswordBackendObserver.php new file mode 100644 index 0000000..e11e48a --- /dev/null +++ b/Observer/CheckUserForgotPasswordBackendObserver.php @@ -0,0 +1,92 @@ +_helper = $helper; + $this->captchaStringResolver = $captchaStringResolver; + $this->_session = $session; + $this->_actionFlag = $actionFlag; + $this->messageManager = $messageManager; + } + + /** + * Check Captcha On User Login Backend Page + * + * @param \Magento\Framework\Event\Observer $observer + * @throws \Magento\Framework\Exception\Plugin\AuthenticationException + * @return $this + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $formId = 'backend_forgotpassword'; + $captchaModel = $this->_helper->getCaptcha($formId); + $controller = $observer->getControllerAction(); + $email = (string)$observer->getControllerAction()->getRequest()->getParam('email'); + $params = $observer->getControllerAction()->getRequest()->getParams(); + if (!empty($email) + && !empty($params) + && $captchaModel->isRequired() + && !$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId)) + ) { + $this->_session->setEmail((string)$controller->getRequest()->getPost('email')); + $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); + $controller->getResponse()->setRedirect( + $controller->getUrl('*/*/forgotpassword', ['_nosecret' => true]) + ); + } + + return $this; + } +} diff --git a/Observer/CheckUserLoginBackendObserver.php b/Observer/CheckUserLoginBackendObserver.php new file mode 100644 index 0000000..924514c --- /dev/null +++ b/Observer/CheckUserLoginBackendObserver.php @@ -0,0 +1,65 @@ +_helper = $helper; + $this->captchaStringResolver = $captchaStringResolver; + $this->_request = $request; + } + + /** + * Check Captcha On User Login Backend Page + * + * @param \Magento\Framework\Event\Observer $observer + * @throws \Magento\Framework\Exception\Plugin\AuthenticationException + * @return $this + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $formId = 'backend_login'; + $captchaModel = $this->_helper->getCaptcha($formId); + $login = $observer->getEvent()->getUsername(); + if ($captchaModel->isRequired($login) + && !$captchaModel->isCorrect($this->captchaStringResolver->resolve($this->_request, $formId)) + ) { + $captchaModel->logAttempt($login); + throw new PluginAuthenticationException(__('Incorrect CAPTCHA.')); + } + $captchaModel->logAttempt($login); + + return $this; + } +} diff --git a/Observer/CheckUserLoginObserver.php b/Observer/CheckUserLoginObserver.php new file mode 100644 index 0000000..2750742 --- /dev/null +++ b/Observer/CheckUserLoginObserver.php @@ -0,0 +1,163 @@ +_helper = $helper; + $this->_actionFlag = $actionFlag; + $this->messageManager = $messageManager; + $this->_session = $customerSession; + $this->captchaStringResolver = $captchaStringResolver; + $this->_customerUrl = $customerUrl; + } + + /** + * Get customer repository + * + * @return \Magento\Customer\Api\CustomerRepositoryInterface + */ + private function getCustomerRepository() + { + + if (!($this->customerRepository instanceof \Magento\Customer\Api\CustomerRepositoryInterface)) { + return \Magento\Framework\App\ObjectManager::getInstance()->get( + \Magento\Customer\Api\CustomerRepositoryInterface::class + ); + } else { + return $this->customerRepository; + } + } + + /** + * Get authentication + * + * @return AuthenticationInterface + */ + private function getAuthentication() + { + + if (!($this->authentication instanceof AuthenticationInterface)) { + return \Magento\Framework\App\ObjectManager::getInstance()->get( + AuthenticationInterface::class + ); + } else { + return $this->authentication; + } + } + + /** + * Check captcha on user login page + * + * @param \Magento\Framework\Event\Observer $observer + * @return $this|void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $formId = 'user_login'; + $captchaModel = $this->_helper->getCaptcha($formId); + $controller = $observer->getControllerAction(); + $loginParams = $controller->getRequest()->getPost('login'); + $login = (is_array($loginParams) && array_key_exists('username', $loginParams)) + ? $loginParams['username'] + : null; + if ($captchaModel->isRequired($login)) { + $word = $this->captchaStringResolver->resolve($controller->getRequest(), $formId); + if (!$captchaModel->isCorrect($word)) { + try { + $customer = $this->getCustomerRepository()->get($login); + $this->getAuthentication()->processAuthenticationFailure($customer->getId()); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock + } catch (NoSuchEntityException $e) { + //do nothing as customer existence is validated later in authenticate method + } + $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA')); + $this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); + $this->_session->setUsername($login); + $beforeUrl = $this->_session->getBeforeAuthUrl(); + $url = $beforeUrl ? $beforeUrl : $this->_customerUrl->getLoginUrl(); + $controller->getResponse()->setRedirect($url); + } + } + $captchaModel->logAttempt($login); + + return $this; + } +} diff --git a/Observer/ResetAttemptForBackendObserver.php b/Observer/ResetAttemptForBackendObserver.php new file mode 100644 index 0000000..376bffb --- /dev/null +++ b/Observer/ResetAttemptForBackendObserver.php @@ -0,0 +1,36 @@ +resLogFactory = $resLogFactory; + } + + /** + * Reset Attempts For Backend + * + * @param \Magento\Framework\Event\Observer $observer + * @return \Magento\Captcha\Observer\ResetAttemptForBackendObserver + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + return $this->resLogFactory->create()->deleteUserAttempts($observer->getUser()->getUsername()); + } +} diff --git a/Observer/ResetAttemptForFrontendAccountEditObserver.php b/Observer/ResetAttemptForFrontendAccountEditObserver.php new file mode 100644 index 0000000..8cf4f1d --- /dev/null +++ b/Observer/ResetAttemptForFrontendAccountEditObserver.php @@ -0,0 +1,55 @@ +helper = $helper; + $this->resLogFactory = $resLogFactory; + } + + /** + * Reset Attempts For Frontend + * + * @param \Magento\Framework\Event\Observer $observer + * @return \Magento\Captcha\Observer\ResetAttemptForFrontendObserver + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $email = $observer->getEmail(); + $captchaModel = $this->helper->getCaptcha(self::FORM_ID); + $captchaModel->setShowCaptchaInSession(false); + + return $this->resLogFactory->create()->deleteUserAttempts($email); + } +} diff --git a/Observer/ResetAttemptForFrontendObserver.php b/Observer/ResetAttemptForFrontendObserver.php new file mode 100644 index 0000000..dedb57a --- /dev/null +++ b/Observer/ResetAttemptForFrontendObserver.php @@ -0,0 +1,39 @@ +resLogFactory = $resLogFactory; + } + + /** + * Reset Attempts For Frontend + * + * @param \Magento\Framework\Event\Observer $observer + * @return \Magento\Captcha\Observer\ResetAttemptForFrontendObserver + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + /** @var \Magento\Customer\Model\Customer $model */ + $model = $observer->getModel(); + + return $this->resLogFactory->create()->deleteUserAttempts($model->getEmail()); + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..35979fb --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +The Captcha module allows applying Turing test in the process of user authentication or similar tasks. \ No newline at end of file diff --git a/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml b/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml new file mode 100644 index 0000000..b84977b --- /dev/null +++ b/Test/Mftf/ActionGroup/AdminLoginWithCaptchaActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + + EXTENDS: LoginAsAdmin. Fills in the Captcha field on the Backend Admin Login page. + + + + + + + + diff --git a/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml b/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml new file mode 100644 index 0000000..c33bcb4 --- /dev/null +++ b/Test/Mftf/ActionGroup/AssertCaptchaNotVisibleOnCustomerLoginFormActionGroup.xml @@ -0,0 +1,26 @@ + + + + + + + Validate that the Captcha is NOT present on the Backend Admin Login page. + + + + + + + + + + + + + diff --git a/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml b/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml new file mode 100644 index 0000000..135b687 --- /dev/null +++ b/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnAdminLoginFormActionGroup.xml @@ -0,0 +1,25 @@ + + + + + + + Validate that the Captcha IS present on the Backend Admin Login page. + + + + + + + + + + + + diff --git a/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml b/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml new file mode 100644 index 0000000..6f61083 --- /dev/null +++ b/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnContactUsFormActionGroup.xml @@ -0,0 +1,25 @@ + + + + + + + Validate that the Captcha IS present on the Storefront Contact Us page. + + + + + + + + + + + + diff --git a/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml b/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml new file mode 100644 index 0000000..bf2f5c2 --- /dev/null +++ b/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountCreatePageActionGroup.xml @@ -0,0 +1,20 @@ + + + + + + + Validate that the Captcha IS present on the Storefront Create Customer page. + + + + + + + diff --git a/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml b/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml new file mode 100644 index 0000000..c2af71c --- /dev/null +++ b/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerAccountInfoActionGroup.xml @@ -0,0 +1,27 @@ + + + + + + + Validate that the Captcha IS present on the Storefront Customer Account Information page. + + + + + + + + + + + + + + diff --git a/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml b/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml new file mode 100644 index 0000000..a53f8f7 --- /dev/null +++ b/Test/Mftf/ActionGroup/AssertCaptchaVisibleOnCustomerLoginFormActionGroup.xml @@ -0,0 +1,25 @@ + + + + + + + Validate that the Captcha IS present on the Storefront Customer Login page. + + + + + + + + + + + + diff --git a/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml b/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml new file mode 100644 index 0000000..52c4181 --- /dev/null +++ b/Test/Mftf/ActionGroup/CaptchaFormsDisplayingActionGroup.xml @@ -0,0 +1,27 @@ + + + + + + + Navigates to store configuration page through UI and expands the CAPTCHA section under Customers > Customer Configuration. + + + + + + + + + + + + + + diff --git a/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml b/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml new file mode 100644 index 0000000..662fdf5 --- /dev/null +++ b/Test/Mftf/ActionGroup/StorefrontCustomerChangeEmailWithCaptchaActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + + EXTENDS: StorefrontCustomerChangeEmailActionGroup. Fills in the Captcha field on the Storefront Customer Information page. + + + + + + + + diff --git a/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml b/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml new file mode 100644 index 0000000..70de13b --- /dev/null +++ b/Test/Mftf/ActionGroup/StorefrontFillContactUsFormWithCaptchaActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + + EXTENDS: StorefrontFillContactUsFormActionGroup. Fills in the Captcha field on the Storefront Contact Us page. + + + + + + + + diff --git a/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml b/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml new file mode 100644 index 0000000..6f45f1b --- /dev/null +++ b/Test/Mftf/ActionGroup/StorefrontFillCustomerAccountCreationFormWithCaptchaActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + + EXTENDS: StorefrontFillCustomerAccountCreationFormActionGroup. Fills in the Captcha field on the Storefront Create Customer page. + + + + + + + + diff --git a/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml b/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml new file mode 100644 index 0000000..ace3132 --- /dev/null +++ b/Test/Mftf/ActionGroup/StorefrontFillCustomerLoginFormWithCaptchaActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + + EXTENDS: StorefrontFillCustomerLoginFormActionGroup. Fills in the Captcha field on the Storefront Customer Login page. + + + + + + + + diff --git a/Test/Mftf/Data/CaptchaConfigData.xml b/Test/Mftf/Data/CaptchaConfigData.xml new file mode 100644 index 0000000..90f48c3 --- /dev/null +++ b/Test/Mftf/Data/CaptchaConfigData.xml @@ -0,0 +1,142 @@ + + + + + + + customer/captcha/enable + 0 + Yes + 1 + + + customer/captcha/enable + 0 + No + 0 + + + customer/captcha/forms + 0 + Create user + user_create + + + customer/captcha/forms + 0 + Contact Us + contact_us + + + + customer/captcha/forms + 0 + Login + user_login + + + customer/captcha/forms + 0 + Change password + user_edit + + + + customer/captcha/forms + 0 + Forgot password + user_forgotpassword + + + customer/captcha/mode + 0 + Always + always + + + + customer/captcha/mode + 0 + After number of attempts to login + after_fail + + + customer/captcha/length + admin + 1 + 3 + 3 + + + customer/captcha/symbols + admin + 1 + 1 + 1 + + + + customer/captcha/length + admin + 1 + 4-5 + 4-5 + + + + customer/captcha/symbols + admin + 1 + ABCDEFGHJKMnpqrstuvwxyz23456789 + ABCDEFGHJKMnpqrstuvwxyz23456789 + + + + admin/captcha/enable + 0 + Yes + 1 + + + admin/captcha/enable + 0 + No + 0 + + + admin/captcha/length + admin + 1 + 3 + 3 + + + admin/captcha/symbols + admin + 1 + 1 + 1 + + + + admin/captcha/length + admin + 1 + 4-5 + 4-5 + + + + admin/captcha/symbols + admin + 1 + ABCDEFGHJKMnpqrstuvwxyz23456789 + ABCDEFGHJKMnpqrstuvwxyz23456789 + + diff --git a/Test/Mftf/Data/CaptchaData.xml b/Test/Mftf/Data/CaptchaData.xml new file mode 100644 index 0000000..d8fb206 --- /dev/null +++ b/Test/Mftf/Data/CaptchaData.xml @@ -0,0 +1,19 @@ + + + + + + WrongCAPTCHA + + + + + 111 + + diff --git a/Test/Mftf/Data/CaptchaFormsDisplayingData.xml b/Test/Mftf/Data/CaptchaFormsDisplayingData.xml new file mode 100644 index 0000000..57a0921 --- /dev/null +++ b/Test/Mftf/Data/CaptchaFormsDisplayingData.xml @@ -0,0 +1,20 @@ + + + + + + Create user + Login + Forgot password + Contact Us + Change password + Register during Checkout + Check Out as Guest + + diff --git a/Test/Mftf/LICENSE.txt b/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000..49525fd --- /dev/null +++ b/Test/Mftf/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/Test/Mftf/LICENSE_AFL.txt b/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000..f39d641 --- /dev/null +++ b/Test/Mftf/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/Test/Mftf/README.md b/Test/Mftf/README.md new file mode 100644 index 0000000..48be768 --- /dev/null +++ b/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Captcha Functional Tests + +The Functional Test Module for **Magento Captcha** module. diff --git a/Test/Mftf/Section/AdminLoginFormSection.xml b/Test/Mftf/Section/AdminLoginFormSection.xml new file mode 100644 index 0000000..2bcc6fc --- /dev/null +++ b/Test/Mftf/Section/AdminLoginFormSection.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml b/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml new file mode 100644 index 0000000..4c974e6 --- /dev/null +++ b/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml b/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml new file mode 100644 index 0000000..f587812 --- /dev/null +++ b/Test/Mftf/Section/StorefrontContactUsCaptchaSection.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/Test/Mftf/Section/StorefrontContactUsFormSection.xml b/Test/Mftf/Section/StorefrontContactUsFormSection.xml new file mode 100644 index 0000000..60cf961 --- /dev/null +++ b/Test/Mftf/Section/StorefrontContactUsFormSection.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml b/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml new file mode 100644 index 0000000..a273c8d --- /dev/null +++ b/Test/Mftf/Section/StorefrontCustomerAccountInformationSection.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml b/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml new file mode 100644 index 0000000..f48e612 --- /dev/null +++ b/Test/Mftf/Section/StorefrontCustomerCreateFormSection.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml b/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml new file mode 100644 index 0000000..54aa36d --- /dev/null +++ b/Test/Mftf/Section/StorefrontCustomerSignInFormSection.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml b/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml new file mode 100644 index 0000000..7a0557c --- /dev/null +++ b/Test/Mftf/Section/StorefrontCustomerSignInPopupFormSection.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml b/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml new file mode 100644 index 0000000..e5ee559 --- /dev/null +++ b/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml b/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml new file mode 100644 index 0000000..8f9c582 --- /dev/null +++ b/Test/Mftf/Test/AdminResetUserPasswordFailedTest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml b/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml new file mode 100644 index 0000000..977ee78 --- /dev/null +++ b/Test/Mftf/Test/CaptchaFormsDisplayingTest.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + {{CaptchaData.checkoutAsGuest}} + $formItems + + + {{CaptchaData.register}} + $formItems + + + + + {{CaptchaData.createUser}} + $createUser + + + + {{CaptchaData.login}} + login + + + + {{CaptchaData.passwd}} + $forgotpassword + + + + {{CaptchaData.contactUs}} + $contactUs + + + + {{CaptchaData.changePasswd}} + $userEdit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml b/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml new file mode 100644 index 0000000..5423708 --- /dev/null +++ b/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml b/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml new file mode 100644 index 0000000..0c6a3f3 --- /dev/null +++ b/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml b/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml new file mode 100644 index 0000000..5a1be68 --- /dev/null +++ b/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml b/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml new file mode 100644 index 0000000..2c331f9 --- /dev/null +++ b/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml b/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml new file mode 100644 index 0000000..36d7989 --- /dev/null +++ b/Test/Mftf/Test/StorefrontResetCustomerPasswordFailedTest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/Test/Unit/Controller/Refresh/IndexTest.php b/Test/Unit/Controller/Refresh/IndexTest.php new file mode 100644 index 0000000..99ac2e2 --- /dev/null +++ b/Test/Unit/Controller/Refresh/IndexTest.php @@ -0,0 +1,137 @@ +captchaHelperMock = $this->createMock(\Magento\Captcha\Helper\Data::class); + $this->captchaMock = $this->createMock(\Magento\Captcha\Model\DefaultModel::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->responseMock = $this->createMock(\Magento\Framework\App\Response\Http::class); + $this->contextMock = $this->createMock(\Magento\Framework\App\Action\Context::class); + $this->viewMock = $this->createMock(\Magento\Framework\App\ViewInterface::class); + $this->layoutMock = $this->createMock(\Magento\Framework\View\LayoutInterface::class); + $this->flagMock = $this->createMock(\Magento\Framework\App\ActionFlag::class); + $this->serializerMock = $this->createMock(\Magento\Framework\Serialize\Serializer\Json::class); + + $this->contextMock->expects($this->any())->method('getRequest')->will($this->returnValue($this->requestMock)); + $this->contextMock->expects($this->any())->method('getView')->will($this->returnValue($this->viewMock)); + $this->contextMock->expects($this->any())->method('getResponse')->will($this->returnValue($this->responseMock)); + $this->contextMock->expects($this->any())->method('getActionFlag')->will($this->returnValue($this->flagMock)); + $this->viewMock->expects($this->any())->method('getLayout')->will($this->returnValue($this->layoutMock)); + + $this->model = new \Magento\Captcha\Controller\Refresh\Index( + $this->contextMock, + $this->captchaHelperMock, + $this->serializerMock + ); + } + + /** + * @dataProvider executeDataProvider + * @param int $formId + * @param int $callsNumber + */ + public function testExecute($formId, $callsNumber) + { + $content = ['formId' => $formId]; + $imgSource = ['imgSrc' => 'source']; + + $blockMethods = ['setFormId', 'setIsAjax', 'toHtml']; + $blockMock = $this->createPartialMock(\Magento\Captcha\Block\Captcha::class, $blockMethods); + + $this->requestMock->expects($this->any())->method('getPost')->with('formId')->will($this->returnValue($formId)); + $this->requestMock->expects($this->exactly($callsNumber))->method('getContent') + ->will($this->returnValue(json_encode($content))); + $this->captchaHelperMock->expects($this->any())->method('getCaptcha')->with($formId) + ->will($this->returnValue($this->captchaMock)); + $this->captchaMock->expects($this->once())->method('generate'); + $this->captchaMock->expects($this->once())->method('getBlockName')->will($this->returnValue('block')); + $this->captchaMock->expects($this->once())->method('getImgSrc')->will($this->returnValue('source')); + $this->layoutMock->expects($this->once())->method('createBlock')->with('block') + ->will($this->returnValue($blockMock)); + $blockMock->expects($this->any())->method('setFormId')->with($formId)->will($this->returnValue($blockMock)); + $blockMock->expects($this->any())->method('setIsAjax')->with(true)->will($this->returnValue($blockMock)); + $blockMock->expects($this->once())->method('toHtml'); + $this->responseMock->expects($this->once())->method('representJson')->with(json_encode($imgSource)); + $this->flagMock->expects($this->once())->method('set')->with('', 'no-postDispatch', true); + $this->serializerMock->expects($this->exactly($callsNumber)) + ->method('unserialize')->will($this->returnValue($content)); + $this->serializerMock->expects($this->once()) + ->method('serialize')->will($this->returnValue(json_encode($imgSource))); + + $this->model->execute(); + } + + /** + * @return array + */ + public function executeDataProvider() + { + return [ + [ + 'formId' => null, + 'callsNumber' => 1, + ], + [ + 'formId' => 1, + 'callsNumber' => 0, + ] + ]; + } +} diff --git a/Test/Unit/Cron/DeleteExpiredImagesTest.php b/Test/Unit/Cron/DeleteExpiredImagesTest.php new file mode 100644 index 0000000..5544890 --- /dev/null +++ b/Test/Unit/Cron/DeleteExpiredImagesTest.php @@ -0,0 +1,156 @@ +_helper = $this->createMock(\Magento\Captcha\Helper\Data::class); + $this->_adminHelper = $this->createMock(\Magento\Captcha\Helper\Adminhtml\Data::class); + $this->_filesystem = $this->createMock(\Magento\Framework\Filesystem::class); + $this->_directory = $this->createMock(\Magento\Framework\Filesystem\Directory\Write::class); + $this->_storeManager = $this->createMock(\Magento\Store\Model\StoreManager::class); + + $this->_filesystem->expects( + $this->once() + )->method( + 'getDirectoryWrite' + )->will( + $this->returnValue($this->_directory) + ); + + $this->_deleteExpiredImages = new \Magento\Captcha\Cron\DeleteExpiredImages( + $this->_helper, + $this->_adminHelper, + $this->_filesystem, + $this->_storeManager + ); + } + + /** + * @dataProvider getExpiredImages + */ + public function testDeleteExpiredImages($website, $isFile, $filename, $mTime, $timeout) + { + $this->_storeManager->expects( + $this->once() + )->method( + 'getWebsites' + )->will( + $this->returnValue(isset($website) ? [$website] : []) + ); + if (isset($website)) { + $this->_helper->expects( + $this->once() + )->method( + 'getConfig' + )->with( + $this->equalTo('timeout'), + new \PHPUnit\Framework\Constraint\IsIdentical($website->getDefaultStore()) + )->will( + $this->returnValue($timeout) + ); + } else { + $this->_helper->expects($this->never())->method('getConfig'); + } + $this->_adminHelper->expects( + $this->once() + )->method( + 'getConfig' + )->with( + $this->equalTo('timeout'), + new \PHPUnit\Framework\Constraint\IsNull() + )->will( + $this->returnValue($timeout) + ); + + $timesToCall = isset($website) ? 2 : 1; + $this->_directory->expects( + $this->exactly($timesToCall) + )->method( + 'read' + )->will( + $this->returnValue([$filename]) + ); + $this->_directory->expects($this->exactly($timesToCall))->method('isFile')->will($this->returnValue($isFile)); + $this->_directory->expects($this->any())->method('stat')->will($this->returnValue(['mtime' => $mTime])); + + $this->_deleteExpiredImages->execute(); + } + + /** + * @return array + */ + public function getExpiredImages() + { + $website = $this->createPartialMock(\Magento\Store\Model\Website::class, ['__wakeup', 'getDefaultStore']); + $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['__wakeup']); + $website->expects($this->any())->method('getDefaultStore')->will($this->returnValue($store)); + $time = time(); + return [ + [null, true, 'test.png', 50, ($time - 60) / 60, true], + [$website, false, 'test.png', 50, ($time - 60) / 60, false], + [$website, true, 'test.jpg', 50, ($time - 60) / 60, false], + [$website, true, 'test.png', 50, ($time - 20) / 60, false] + ]; + } +} + +/** + * Fix current time + * + * @return int + */ +function time() +{ + if (!isset(DeleteExpiredImagesTest::$currentTime)) { + DeleteExpiredImagesTest::$currentTime = \time(); + } + return DeleteExpiredImagesTest::$currentTime; +} diff --git a/Test/Unit/CustomerData/CaptchaTest.php b/Test/Unit/CustomerData/CaptchaTest.php new file mode 100644 index 0000000..a791039 --- /dev/null +++ b/Test/Unit/CustomerData/CaptchaTest.php @@ -0,0 +1,98 @@ +helperMock = $this->createMock(CaptchaHelper::class); + $this->customerSessionMock = $this->createMock(CustomerSession::class); + $this->formIds = [ + 'user_login' + ]; + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $this->objectManagerHelper->getObject( + Captcha::class, + [ + 'helper' => $this->helperMock, + 'formIds' => $this->formIds, + 'customerSession' => $this->customerSessionMock + ] + ); + } + + /** + * Test getSectionData() when user is login and require captcha + */ + public function testGetSectionDataWhenLoginAndRequireCaptcha() + { + $emailLogin = 'test@localhost.com'; + + $userLoginModel = $this->createMock(DefaultModel::class); + $userLoginModel->expects($this->any())->method('isRequired')->with($emailLogin) + ->willReturn(true); + $this->helperMock->expects($this->any())->method('getCaptcha')->with('user_login')->willReturn($userLoginModel); + + $this->customerSessionMock->expects($this->any())->method('isLoggedIn') + ->willReturn(true); + + $customerDataMock = $this->createMock(CustomerData::class); + $customerDataMock->expects($this->any())->method('getEmail')->willReturn($emailLogin); + $this->customerSessionMock->expects($this->any())->method('getCustomerData') + ->willReturn($customerDataMock); + + /* Assert to test */ + $this->assertEquals( + [ + "user_login" => [ + "isRequired" => true, + "timestamp" => time() + ] + ], + $this->model->getSectionData() + ); + } +} diff --git a/Test/Unit/Helper/Adminhtml/DataTest.php b/Test/Unit/Helper/Adminhtml/DataTest.php new file mode 100644 index 0000000..b7a38f0 --- /dev/null +++ b/Test/Unit/Helper/Adminhtml/DataTest.php @@ -0,0 +1,56 @@ +getConstructArguments($className); + + $backendConfig = $arguments['backendConfig']; + $backendConfig->expects( + $this->any() + )->method( + 'getValue' + )->with( + 'admin/captcha/qwe' + )->will( + $this->returnValue('1') + ); + + $filesystemMock = $arguments['filesystem']; + $directoryMock = $this->createMock(\Magento\Framework\Filesystem\Directory\Write::class); + + $filesystemMock->expects($this->any())->method('getDirectoryWrite')->will($this->returnValue($directoryMock)); + $directoryMock->expects($this->any())->method('getAbsolutePath')->will($this->returnArgument(0)); + + $this->_model = $objectManagerHelper->getObject($className, $arguments); + } + + public function testGetConfig() + { + $this->assertEquals('1', $this->_model->getConfig('qwe')); + } + + /** + * @covers \Magento\Captcha\Helper\Adminhtml\Data::_getWebsiteCode + */ + public function testGetWebsiteId() + { + $this->assertStringEndsWith('/admin/', $this->_model->getImgDir()); + } +} diff --git a/Test/Unit/Helper/DataTest.php b/Test/Unit/Helper/DataTest.php new file mode 100644 index 0000000..8103021 --- /dev/null +++ b/Test/Unit/Helper/DataTest.php @@ -0,0 +1,215 @@ +getConstructArguments($className); + /** @var \Magento\Framework\App\Helper\Context $context */ + $context = $arguments['context']; + $this->configMock = $context->getScopeConfig(); + $this->_filesystem = $arguments['filesystem']; + $storeManager = $arguments['storeManager']; + $storeManager->expects($this->any())->method('getWebsite')->will($this->returnValue($this->_getWebsiteStub())); + $storeManager->expects($this->any())->method('getStore')->will($this->returnValue($this->_getStoreStub())); + $this->factoryMock = $arguments['factory']; + $this->helper = $objectManagerHelper->getObject($className, $arguments); + } + + /** + * @covers \Magento\Captcha\Helper\Data::getCaptcha + */ + public function testGetCaptcha() + { + $this->configMock->expects( + $this->once() + )->method( + 'getValue' + )->with( + 'customer/captcha/type' + )->will( + $this->returnValue('zend') + ); + + $this->factoryMock->expects( + $this->once() + )->method( + 'create' + )->with( + $this->equalTo('Zend') + )->will( + $this->returnValue( + new \Magento\Captcha\Model\DefaultModel( + $this->createMock(\Magento\Framework\Session\SessionManager::class), + $this->createMock(\Magento\Captcha\Helper\Data::class), + $this->createPartialMock(\Magento\Captcha\Model\ResourceModel\LogFactory::class, ['create']), + 'user_create' + ) + ) + ); + + $this->assertInstanceOf(\Magento\Captcha\Model\DefaultModel::class, $this->helper->getCaptcha('user_create')); + } + + /** + * @covers \Magento\Captcha\Helper\Data::getConfig + */ + public function testGetConfigNode() + { + $this->configMock->expects( + $this->once() + )->method( + 'getValue' + )->with( + 'customer/captcha/enable', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + )->will( + $this->returnValue('1') + ); + + $this->helper->getConfig('enable'); + } + + public function testGetFonts() + { + $fontPath = 'path/to/fixture.ttf'; + $expectedFontPath = 'lib/' . $fontPath; + + $libDirMock = $this->createMock(\Magento\Framework\Filesystem\Directory\Read::class); + $libDirMock->expects($this->once()) + ->method('getAbsolutePath') + ->with($fontPath) + ->will($this->returnValue($expectedFontPath)); + $this->_filesystem->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::LIB_INTERNAL) + ->will($this->returnValue($libDirMock)); + + $configData = ['font_code' => ['label' => 'Label', 'path' => $fontPath]]; + + $this->configMock->expects( + $this->any() + )->method( + 'getValue' + )->with( + 'captcha/fonts', + 'default' + )->will( + $this->returnValue($configData) + ); + + $fonts = $this->helper->getFonts(); + $this->assertArrayHasKey('font_code', $fonts); + // fixture + $this->assertArrayHasKey('label', $fonts['font_code']); + $this->assertArrayHasKey('path', $fonts['font_code']); + $this->assertEquals('Label', $fonts['font_code']['label']); + $this->assertEquals($expectedFontPath, $fonts['font_code']['path']); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::getImgDir + * @covers \Magento\Captcha\Helper\Data::getImgDir + */ + public function testGetImgDir() + { + $dirWriteMock = $this->createPartialMock( + \Magento\Framework\Filesystem\Directory\Write::class, + ['changePermissions', 'create', 'getAbsolutePath'] + ); + + $this->_filesystem->expects( + $this->once() + )->method( + 'getDirectoryWrite' + )->with( + DirectoryList::MEDIA + )->will( + $this->returnValue($dirWriteMock) + ); + + $dirWriteMock->expects( + $this->once() + )->method( + 'getAbsolutePath' + )->with( + '/captcha/base' + )->will( + $this->returnValue(TESTS_TEMP_DIR . '/captcha/base') + ); + + $this->assertFileNotExists(TESTS_TEMP_DIR . '/captcha'); + $result = $this->helper->getImgDir(); + $this->assertStringStartsWith(TESTS_TEMP_DIR, $result); + $this->assertStringEndsWith('captcha/base/', $result); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::getImgUrl + * @covers \Magento\Captcha\Helper\Data::getImgUrl + */ + public function testGetImgUrl() + { + $this->assertEquals($this->helper->getImgUrl(), 'http://localhost/pub/media/captcha/base/'); + } + + /** + * Create Website Stub + * + * @return \Magento\Store\Model\Website + */ + protected function _getWebsiteStub() + { + $website = $this->createPartialMock(\Magento\Store\Model\Website::class, ['getCode', '__wakeup']); + + $website->expects($this->any())->method('getCode')->will($this->returnValue('base')); + + return $website; + } + + /** + * Create store stub + * + * @return \Magento\Store\Model\Store + */ + protected function _getStoreStub() + { + $store = $this->createMock(\Magento\Store\Model\Store::class); + + $store->expects($this->any())->method('getBaseUrl')->will($this->returnValue('http://localhost/pub/media/')); + + return $store; + } +} diff --git a/Test/Unit/Model/CaptchaFactoryTest.php b/Test/Unit/Model/CaptchaFactoryTest.php new file mode 100644 index 0000000..2890f10 --- /dev/null +++ b/Test/Unit/Model/CaptchaFactoryTest.php @@ -0,0 +1,63 @@ +_objectManagerMock = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); + $this->_model = new \Magento\Captcha\Model\CaptchaFactory($this->_objectManagerMock); + } + + public function testCreatePositive() + { + $captchaType = 'default'; + + $defaultCaptchaMock = $this->createMock(\Magento\Captcha\Model\DefaultModel::class); + + $this->_objectManagerMock->expects( + $this->once() + )->method( + 'create' + )->with( + $this->equalTo('Magento\Captcha\Model\\' . ucfirst($captchaType)) + )->will( + $this->returnValue($defaultCaptchaMock) + ); + + $this->assertEquals($defaultCaptchaMock, $this->_model->create($captchaType, 'form_id')); + } + + public function testCreateNegative() + { + $captchaType = 'wrong_instance'; + + $defaultCaptchaMock = $this->createMock(\stdClass::class); + + $this->_objectManagerMock->expects( + $this->once() + )->method( + 'create' + )->with( + $this->equalTo('Magento\Captcha\Model\\' . ucfirst($captchaType)) + )->will( + $this->returnValue($defaultCaptchaMock) + ); + + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Magento\Captcha\Model\\' . ucfirst($captchaType) . + ' does not implement \Magento\Captcha\Model\CaptchaInterface'); + + $this->assertEquals($defaultCaptchaMock, $this->_model->create($captchaType, 'form_id')); + } +} diff --git a/Test/Unit/Model/Cart/ConfigPluginTest.php b/Test/Unit/Model/Cart/ConfigPluginTest.php new file mode 100644 index 0000000..e24eec2 --- /dev/null +++ b/Test/Unit/Model/Cart/ConfigPluginTest.php @@ -0,0 +1,46 @@ +configProviderMock = $this->createMock(\Magento\Captcha\Model\Checkout\ConfigProvider::class); + $this->model = new \Magento\Captcha\Model\Cart\ConfigPlugin( + $this->configProviderMock + ); + } + + public function testAfterGetConfig() + { + $resultMock = [ + 'result' => [ + 'data' => 'resultDataMock' + ] + ]; + $configMock = [ + 'config' => [ + 'data' => 'configDataMock' + ] + ]; + $expectedResult = array_merge_recursive($resultMock, $configMock); + $sidebarMock = $this->createMock(\Magento\Checkout\Block\Cart\Sidebar::class); + $this->configProviderMock->expects($this->once())->method('getConfig')->willReturn($configMock); + + $this->assertEquals($expectedResult, $this->model->afterGetConfig($sidebarMock, $resultMock)); + } +} diff --git a/Test/Unit/Model/Checkout/ConfigProviderTest.php b/Test/Unit/Model/Checkout/ConfigProviderTest.php new file mode 100644 index 0000000..8764dbd --- /dev/null +++ b/Test/Unit/Model/Checkout/ConfigProviderTest.php @@ -0,0 +1,122 @@ +storeManagerMock = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class); + $this->captchaHelperMock = $this->createMock(\Magento\Captcha\Helper\Data::class); + $this->captchaMock = $this->createMock(\Magento\Captcha\Model\DefaultModel::class); + $this->storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $formIds = [$this->formId]; + + $this->model = new \Magento\Captcha\Model\Checkout\ConfigProvider( + $this->storeManagerMock, + $this->captchaHelperMock, + $formIds + ); + } + + /** + * @dataProvider getConfigDataProvider + * @param bool $isRequired + * @param integer $captchaGenerations + * @param array $expectedConfig + */ + public function testGetConfig($isRequired, $captchaGenerations, $expectedConfig) + { + $this->captchaHelperMock->expects($this->any())->method('getCaptcha')->with($this->formId) + ->will($this->returnValue($this->captchaMock)); + + $this->captchaMock->expects($this->any())->method('isCaseSensitive')->will($this->returnValue(1)); + $this->captchaMock->expects($this->any())->method('getHeight')->will($this->returnValue('12px')); + $this->captchaMock->expects($this->any())->method('isRequired')->will($this->returnValue($isRequired)); + + $this->captchaMock->expects($this->exactly($captchaGenerations))->method('generate'); + $this->captchaMock->expects($this->exactly($captchaGenerations))->method('getImgSrc') + ->will($this->returnValue('source')); + + $this->storeManagerMock->expects($this->any())->method('getStore')->will($this->returnValue($this->storeMock)); + $this->storeMock->expects($this->once())->method('isCurrentlySecure')->will($this->returnValue(true)); + $this->storeMock->expects($this->once())->method('getUrl')->with('captcha/refresh', ['_secure' => true]) + ->will($this->returnValue('https://magento.com/captcha')); + + $config = $this->model->getConfig(); + unset($config['captcha'][$this->formId]['timestamp']); + $this->assertEquals($config, $expectedConfig); + } + + /** + * @return array + */ + public function getConfigDataProvider() + { + return [ + [ + 'isRequired' => true, + 'captchaGenerations' => 1, + 'expectedConfig' => [ + 'captcha' => [ + $this->formId => [ + 'isCaseSensitive' => true, + 'imageHeight' => '12px', + 'imageSrc' => 'source', + 'refreshUrl' => 'https://magento.com/captcha', + 'isRequired' => true + ], + ], + ], + ], + [ + 'isRequired' => false, + 'captchaGenerations' => 0, + 'expectedConfig' => [ + 'captcha' => [ + $this->formId => [ + 'isCaseSensitive' => true, + 'imageHeight' => '12px', + 'imageSrc' => '', + 'refreshUrl' => 'https://magento.com/captcha', + 'isRequired' => false + ], + ], + ], + ], + ]; + } +} diff --git a/Test/Unit/Model/Config/FontTest.php b/Test/Unit/Model/Config/FontTest.php new file mode 100644 index 0000000..42ab314 --- /dev/null +++ b/Test/Unit/Model/Config/FontTest.php @@ -0,0 +1,101 @@ +helperDataMock = $this->createMock(HelperData::class); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->model = $this->objectManagerHelper->getObject( + Font::class, + [ + 'captchaData' => $this->helperDataMock + ] + ); + } + + /** + * Test toOptionArray() with data provider below + * + * @param array $fonts + * @param array $expectedResult + * @dataProvider toOptionArrayDataProvider + */ + public function testToOptionArray($fonts, $expectedResult) + { + $this->helperDataMock->expects($this->any())->method('getFonts') + ->willReturn($fonts); + + $this->assertEquals($expectedResult, $this->model->toOptionArray()); + } + + /** + * Data Provider for testing toOptionArray() + * + * @return array + */ + public function toOptionArrayDataProvider() + { + return [ + 'Empty get font' => [ + [], + [] + ], + 'Get font result' => [ + [ + 'arial' => [ + 'label' => 'Arial', + 'path' => '/www/magento/fonts/arial.ttf' + ], + 'verdana' => [ + 'label' => 'Verdana', + 'path' => '/www/magento/fonts/verdana.ttf' + ] + ], + [ + [ + 'label' => 'Arial', + 'value' => 'arial' + ], + [ + 'label' => 'Verdana', + 'value' => 'verdana' + ] + ] + ] + ]; + } +} diff --git a/Test/Unit/Model/Config/Form/BackendTest.php b/Test/Unit/Model/Config/Form/BackendTest.php new file mode 100644 index 0000000..054cc71 --- /dev/null +++ b/Test/Unit/Model/Config/Form/BackendTest.php @@ -0,0 +1,100 @@ +configMock = $this->createMock(ScopeConfigInterface::class); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->model = $this->objectManagerHelper->getObject( + Backend::class, + [ + 'config' => $this->configMock + ] + ); + } + + /** + * Test toOptionArray() with data provider below + * + * @param string|array $config + * @param array $expectedResult + * @dataProvider toOptionArrayDataProvider + */ + public function testToOptionArray($config, $expectedResult) + { + $this->configMock->expects($this->any())->method('getValue') + ->with('captcha/backend/areas', 'default') + ->willReturn($config); + + $this->assertEquals($expectedResult, $this->model->toOptionArray()); + } + + /** + * Data Provider for testing toOptionArray() + * + * @return array + */ + public function toOptionArrayDataProvider() + { + return [ + 'Empty captcha backend areas' => [ + '', + [] + ], + 'With two captcha backend area' => [ + [ + 'backend_login' => [ + 'label' => 'Admin Login' + ], + 'backend_forgotpassword' => [ + 'label' => 'Admin Forgot Password' + ] + ], + [ + [ + 'label' => 'Admin Login', + 'value' => 'backend_login' + ], + [ + 'label' => 'Admin Forgot Password', + 'value' => 'backend_forgotpassword' + ] + ] + ] + ]; + } +} diff --git a/Test/Unit/Model/Config/Form/FrontendTest.php b/Test/Unit/Model/Config/Form/FrontendTest.php new file mode 100644 index 0000000..d3f40f5 --- /dev/null +++ b/Test/Unit/Model/Config/Form/FrontendTest.php @@ -0,0 +1,100 @@ +configMock = $this->createMock(ScopeConfigInterface::class); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->model = $this->objectManagerHelper->getObject( + Frontend::class, + [ + 'config' => $this->configMock + ] + ); + } + + /** + * Test toOptionArray() with data provider below + * + * @param string|array $config + * @param array $expectedResult + * @dataProvider toOptionArrayDataProvider + */ + public function testToOptionArray($config, $expectedResult) + { + $this->configMock->expects($this->any())->method('getValue') + ->with('captcha/frontend/areas', 'default') + ->willReturn($config); + + $this->assertEquals($expectedResult, $this->model->toOptionArray()); + } + + /** + * Data Provider for testing toOptionArray() + * + * @return array + */ + public function toOptionArrayDataProvider() + { + return [ + 'Empty captcha frontend areas' => [ + '', + [] + ], + 'With two captcha frontend area' => [ + [ + 'product_sendtofriend_form' => [ + 'label' => 'Send To Friend Form' + ], + 'sales_rule_coupon_request' => [ + 'label' => 'Applying coupon code' + ] + ], + [ + [ + 'label' => 'Send To Friend Form', + 'value' => 'product_sendtofriend_form' + ], + [ + 'label' => 'Applying coupon code', + 'value' => 'sales_rule_coupon_request' + ] + ] + ] + ]; + } +} diff --git a/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php b/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php new file mode 100644 index 0000000..ec2a49f --- /dev/null +++ b/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php @@ -0,0 +1,231 @@ +sessionManagerMock = $this->createPartialMock(\Magento\Checkout\Model\Session::class, ['setUsername']); + $this->captchaHelperMock = $this->createMock(\Magento\Captcha\Helper\Data::class); + $this->captchaMock = $this->createMock(\Magento\Captcha\Model\DefaultModel::class); + $this->jsonFactoryMock = $this->createPartialMock( + \Magento\Framework\Controller\Result\JsonFactory::class, + ['create'] + ); + $this->resultJsonMock = $this->createMock(\Magento\Framework\Controller\Result\Json::class); + $this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->loginControllerMock = $this->createMock(\Magento\Customer\Controller\Ajax\Login::class); + + $this->loginControllerMock->expects($this->any())->method('getRequest') + ->will($this->returnValue($this->requestMock)); + + $this->captchaHelperMock + ->expects($this->exactly(1)) + ->method('getCaptcha') + ->will($this->returnValue($this->captchaMock)); + + $this->formIds = ['user_login']; + $this->serializerMock = $this->createMock(\Magento\Framework\Serialize\Serializer\Json::class); + + $this->model = new \Magento\Captcha\Model\Customer\Plugin\AjaxLogin( + $this->captchaHelperMock, + $this->sessionManagerMock, + $this->jsonFactoryMock, + $this->formIds, + $this->serializerMock + ); + } + + /** + * Test aroundExecute. + */ + public function testAroundExecute() + { + $username = 'name'; + $captchaString = 'string'; + $requestData = [ + 'username' => $username, + 'captcha_string' => $captchaString, + 'captcha_form_id' => $this->formIds[0] + ]; + $requestContent = json_encode($requestData); + + $this->requestMock->expects($this->once())->method('getContent')->will($this->returnValue($requestContent)); + $this->captchaMock->expects($this->once())->method('isRequired')->with($username) + ->will($this->returnValue(true)); + $this->captchaMock->expects($this->once())->method('logAttempt')->with($username); + $this->captchaMock->expects($this->once())->method('isCorrect')->with($captchaString) + ->will($this->returnValue(true)); + $this->serializerMock->expects($this->once())->method('unserialize')->will($this->returnValue($requestData)); + + $closure = function () { + return 'result'; + }; + + $this->captchaHelperMock + ->expects($this->exactly(1)) + ->method('getCaptcha') + ->with('user_login') + ->will($this->returnValue($this->captchaMock)); + + $this->assertEquals('result', $this->model->aroundExecute($this->loginControllerMock, $closure)); + } + + /** + * Test aroundExecuteIncorrectCaptcha. + */ + public function testAroundExecuteIncorrectCaptcha() + { + $username = 'name'; + $captchaString = 'string'; + $requestData = [ + 'username' => $username, + 'captcha_string' => $captchaString, + 'captcha_form_id' => $this->formIds[0] + ]; + $requestContent = json_encode($requestData); + + $this->requestMock->expects($this->once())->method('getContent')->will($this->returnValue($requestContent)); + $this->captchaMock->expects($this->once())->method('isRequired')->with($username) + ->will($this->returnValue(true)); + $this->captchaMock->expects($this->once())->method('logAttempt')->with($username); + $this->captchaMock->expects($this->once())->method('isCorrect') + ->with($captchaString)->will($this->returnValue(false)); + $this->serializerMock->expects($this->once())->method('unserialize')->will($this->returnValue($requestData)); + + $this->sessionManagerMock->expects($this->once())->method('setUsername')->with($username); + $this->jsonFactoryMock->expects($this->once())->method('create') + ->will($this->returnValue($this->resultJsonMock)); + + $this->resultJsonMock + ->expects($this->once()) + ->method('setData') + ->with(['errors' => true, 'message' => __('Incorrect CAPTCHA')]) + ->will($this->returnSelf()); + + $closure = function () { + }; + $this->assertEquals($this->resultJsonMock, $this->model->aroundExecute($this->loginControllerMock, $closure)); + } + + /** + * @dataProvider aroundExecuteCaptchaIsNotRequired + * @param string $username + * @param array $requestContent + */ + public function testAroundExecuteCaptchaIsNotRequired($username, $requestContent) + { + $this->requestMock->expects($this->once())->method('getContent') + ->will($this->returnValue(json_encode($requestContent))); + $this->serializerMock->expects($this->once())->method('unserialize') + ->will($this->returnValue($requestContent)); + + $this->captchaMock->expects($this->once())->method('isRequired')->with($username) + ->will($this->returnValue(false)); + $this->captchaMock->expects($this->never())->method('logAttempt')->with($username); + $this->captchaMock->expects($this->never())->method('isCorrect'); + + $closure = function () { + return 'result'; + }; + $this->assertEquals('result', $this->model->aroundExecute($this->loginControllerMock, $closure)); + } + + /** + * @return array + */ + public function aroundExecuteCaptchaIsNotRequired(): array + { + return [ + [ + 'username' => 'name', + 'requestData' => ['username' => 'name', 'captcha_string' => 'string'], + ], + [ + 'username' => 'name', + 'requestData' => + [ + 'username' => 'name', + 'captcha_string' => 'string', + 'captcha_form_id' => $this->formIds[0] + ], + ], + [ + 'username' => null, + 'requestData' => + [ + 'username' => null, + 'captcha_string' => 'string', + 'captcha_form_id' => $this->formIds[0] + ], + ], + [ + 'username' => 'name', + 'requestData' => + [ + 'username' => 'name', + 'captcha_string' => 'string', + 'captcha_form_id' => null + ], + ], + ]; + } +} diff --git a/Test/Unit/Model/DefaultTest.php b/Test/Unit/Model/DefaultTest.php new file mode 100644 index 0000000..b569803 --- /dev/null +++ b/Test/Unit/Model/DefaultTest.php @@ -0,0 +1,418 @@ + 'default', + 'enable' => '1', + 'font' => 'linlibertine', + 'mode' => 'after_fail', + 'forms' => 'user_forgotpassword,user_create', + 'failed_attempts_login' => '3', + 'failed_attempts_ip' => '1000', + 'timeout' => '7', + 'length' => '4-5', + 'symbols' => 'ABCDEFGHJKMnpqrstuvwxyz23456789', + 'case_sensitive' => '0', + 'shown_to_logged_in_user' => ['contact_us' => 1], + 'always_for' => [ + 'user_create', + 'user_forgotpassword', + 'contact_us', + ], + ]; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $_dirMock; + + /** + * path to fonts + * @var array + */ + protected $_fontPath = [ + 'LinLibertine' => [ + 'label' => 'LinLibertine', + 'path' => 'lib/internal/LinLibertineFont/LinLibertine_Bd-2.8.1.ttf', + ], + ]; + + /** + * @var \Magento\Captcha\Model\DefaultModel + */ + protected $_object; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $_objectManager; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $_storeManager; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $session; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $_resLogFactory; + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + protected function setUp() + { + $this->session = $this->_getSessionStub(); + + $this->_storeManager = $this->createPartialMock(\Magento\Store\Model\StoreManager::class, ['getStore']); + $this->_storeManager->expects( + $this->any() + )->method( + 'getStore' + )->will( + $this->returnValue($this->_getStoreStub()) + ); + + // \Magento\Customer\Model\Session + $this->_objectManager = $this->createMock(\Magento\Framework\ObjectManagerInterface::class); + $this->_objectManager->expects( + $this->any() + )->method( + 'get' + )->will( + $this->returnValueMap( + [ + \Magento\Captcha\Helper\Data::class => $this->_getHelperStub(), + \Magento\Customer\Model\Session::class => $this->session, + ] + ) + ); + + $this->_resLogFactory = $this->createPartialMock( + \Magento\Captcha\Model\ResourceModel\LogFactory::class, + ['create'] + ); + $this->_resLogFactory->expects( + $this->any() + )->method( + 'create' + )->will( + $this->returnValue($this->_getResourceModelStub()) + ); + + $this->_object = new \Magento\Captcha\Model\DefaultModel( + $this->session, + $this->_getHelperStub(), + $this->_resLogFactory, + 'user_create' + ); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::getBlockName + */ + public function testGetBlockName() + { + $this->assertEquals($this->_object->getBlockName(), \Magento\Captcha\Block\Captcha\DefaultCaptcha::class); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::isRequired + */ + public function testIsRequired() + { + $this->assertTrue($this->_object->isRequired()); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::isCaseSensitive + */ + public function testIsCaseSensitive() + { + self::$_defaultConfig['case_sensitive'] = '1'; + $this->assertEquals($this->_object->isCaseSensitive(), '1'); + self::$_defaultConfig['case_sensitive'] = '0'; + $this->assertEquals($this->_object->isCaseSensitive(), '0'); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::getFont + */ + public function testGetFont() + { + $this->assertEquals($this->_object->getFont(), $this->_fontPath['LinLibertine']['path']); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::getTimeout + * @covers \Magento\Captcha\Model\DefaultModel::getExpiration + */ + public function testGetTimeout() + { + $this->assertEquals($this->_object->getTimeout(), self::$_defaultConfig['timeout'] * 60); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::isCorrect + */ + public function testIsCorrect() + { + self::$_defaultConfig['case_sensitive'] = '1'; + $this->assertFalse($this->_object->isCorrect('abcdef5')); + $sessionData = [ + 'user_create_word' => [ + 'data' => 'AbCdEf5', + 'words' => 'AbCdEf5', + 'expires' => time() + self::EXPIRE_FRAME + ] + ]; + $this->_object->getSession()->setData($sessionData); + self::$_defaultConfig['case_sensitive'] = '0'; + $this->assertTrue($this->_object->isCorrect('abcdef5')); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::getImgSrc + */ + public function testGetImgSrc() + { + $this->assertEquals( + $this->_object->getImgSrc(), + 'http://localhost/pub/media/captcha/base/' . $this->_object->getId() . '.png' + ); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::logAttempt + */ + public function testLogAttempt() + { + $captcha = new \Magento\Captcha\Model\DefaultModel( + $this->session, + $this->_getHelperStub(), + $this->_resLogFactory, + 'user_create' + ); + + $captcha->logAttempt('admin'); + + $this->assertEquals($captcha->getSession()->getData('user_create_show_captcha'), 1); + } + + /** + * @covers \Magento\Captcha\Model\DefaultModel::getWord + */ + public function testGetWord() + { + $this->assertEquals($this->_object->getWord(), 'AbCdEf5'); + $this->_object->getSession()->setData( + ['user_create_word' => ['data' => 'AbCdEf5', 'words' => 'AbCdEf5','expires' => time() - 360]] + ); + $this->assertNull($this->_object->getWord()); + } + + /** + * Create stub session object + * + * @return \Magento\Customer\Model\Session + */ + protected function _getSessionStub() + { + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $sessionArgs = $helper->getConstructArguments( + \Magento\Customer\Model\Session::class, + ['storage' => new \Magento\Framework\Session\Storage()] + ); + $session = $this->getMockBuilder(\Magento\Customer\Model\Session::class) + ->setMethods(['isLoggedIn', 'getUserCreateWord']) + ->setConstructorArgs($sessionArgs) + ->getMock(); + $session->expects($this->any())->method('isLoggedIn')->will($this->returnValue(false)); + + $session->setData( + [ + 'user_create_word' => [ + 'data' => 'AbCdEf5', + 'words' => 'AbCdEf5', + 'expires' => time() + self::EXPIRE_FRAME + ] + ] + ); + return $session; + } + + /** + * Create helper stub + * @return \Magento\Captcha\Helper\Data + */ + protected function _getHelperStub() + { + $helper = $this->getMockBuilder( + \Magento\Captcha\Helper\Data::class + )->disableOriginalConstructor()->setMethods( + ['getConfig', 'getFonts', '_getWebsiteCode', 'getImgUrl'] + )->getMock(); + + $helper->expects( + $this->any() + )->method( + 'getConfig' + )->will( + $this->returnCallback('Magento\Captcha\Test\Unit\Model\DefaultTest::getConfigNodeStub') + ); + + $helper->expects($this->any())->method('getFonts')->will($this->returnValue($this->_fontPath)); + + $helper->expects($this->any())->method('_getWebsiteCode')->will($this->returnValue('base')); + + $helper->expects( + $this->any() + )->method( + 'getImgUrl' + )->will( + $this->returnValue('http://localhost/pub/media/captcha/base/') + ); + + return $helper; + } + + /** + * Get stub for resource model + * @return \Magento\Captcha\Model\ResourceModel\Log + */ + protected function _getResourceModelStub() + { + $resourceModel = $this->createPartialMock( + \Magento\Captcha\Model\ResourceModel\Log::class, + ['countAttemptsByRemoteAddress', 'countAttemptsByUserLogin', 'logAttempt', '__wakeup'] + ); + + $resourceModel->expects($this->any())->method('logAttempt'); + + $resourceModel->expects($this->any())->method('countAttemptsByRemoteAddress')->will($this->returnValue(0)); + + $resourceModel->expects($this->any())->method('countAttemptsByUserLogin')->will($this->returnValue(3)); + return $resourceModel; + } + + /** + * Mock get config method + * @static + * @return string + * @throws \InvalidArgumentException + */ + public static function getConfigNodeStub() + { + $args = func_get_args(); + $hashName = $args[0]; + + if (array_key_exists($hashName, self::$_defaultConfig)) { + return self::$_defaultConfig[$hashName]; + } + + throw new \InvalidArgumentException('Unknow id = ' . $hashName); + } + + /** + * Create store stub + * + * @return \Magento\Store\Model\Store + */ + protected function _getStoreStub() + { + $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['isAdmin', 'getBaseUrl']); + $store->expects($this->any())->method('getBaseUrl')->will($this->returnValue('http://localhost/pub/media/')); + $store->expects($this->any())->method('isAdmin')->will($this->returnValue(false)); + return $store; + } + + /** + * @param boolean $expectedResult + * @param string $formId + * @dataProvider isShownToLoggedInUserDataProvider + */ + public function testIsShownToLoggedInUser($expectedResult, $formId) + { + $captcha = new \Magento\Captcha\Model\DefaultModel( + $this->session, + $this->_getHelperStub(), + $this->_resLogFactory, + $formId + ); + $this->assertEquals($expectedResult, $captcha->isShownToLoggedInUser()); + } + + /** + * @return array + */ + public function isShownToLoggedInUserDataProvider() + { + return [ + [true, 'contact_us'], + [false, 'user_create'], + [false, 'user_forgotpassword'] + ]; + } + + /** + * @param string $string + * @dataProvider generateWordProvider + * @throws \ReflectionException + */ + public function testGenerateWord($string) + { + $randomMock = $this->createMock(Random::class); + $randomMock->expects($this->once()) + ->method('getRandomString') + ->will($this->returnValue($string)); + $captcha = new \Magento\Captcha\Model\DefaultModel( + $this->session, + $this->_getHelperStub(), + $this->_resLogFactory, + 'user_create', + $randomMock + ); + $method = new \ReflectionMethod($captcha, 'generateWord'); + $method->setAccessible(true); + $this->assertEquals($string, $method->invoke($captcha)); + } + /** + * @return array + */ + public function generateWordProvider() + { + return [ + ['ABC123'], + ['1234567890'], + ['The quick brown fox jumps over the lazy dog.'] + ]; + } +} diff --git a/Test/Unit/Observer/CaptchaStringResolverTest.php b/Test/Unit/Observer/CaptchaStringResolverTest.php new file mode 100644 index 0000000..2bd8ac6 --- /dev/null +++ b/Test/Unit/Observer/CaptchaStringResolverTest.php @@ -0,0 +1,69 @@ +objectManagerHelper = new ObjectManager($this); + $this->requestMock = $this->createMock(HttpRequest::class); + $this->captchaStringResolver = $this->objectManagerHelper->getObject(CaptchaStringResolver::class); + } + + public function testResolveWithFormIdSet() + { + $formId = 'contact_us'; + $captchaValue = 'some-value'; + + $this->requestMock->expects($this->once()) + ->method('getPost') + ->with(CaptchaDataHelper::INPUT_NAME_FIELD_VALUE) + ->willReturn([$formId => $captchaValue]); + + self::assertEquals( + $this->captchaStringResolver->resolve($this->requestMock, $formId), + $captchaValue + ); + } + + public function testResolveWithNoFormIdInRequest() + { + $formId = 'contact_us'; + + $this->requestMock->expects($this->once()) + ->method('getPost') + ->with(CaptchaDataHelper::INPUT_NAME_FIELD_VALUE) + ->willReturn([]); + + self::assertEquals( + $this->captchaStringResolver->resolve($this->requestMock, $formId), + '' + ); + } +} diff --git a/Test/Unit/Observer/CheckContactUsFormObserverTest.php b/Test/Unit/Observer/CheckContactUsFormObserverTest.php new file mode 100644 index 0000000..83bfb29 --- /dev/null +++ b/Test/Unit/Observer/CheckContactUsFormObserverTest.php @@ -0,0 +1,193 @@ +objectManagerHelper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + + $this->helperMock = $this->createMock(\Magento\Captcha\Helper\Data::class); + $this->actionFlagMock = $this->createMock(\Magento\Framework\App\ActionFlag::class); + $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); + $this->redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); + $this->captchaStringResolverMock = $this->createMock(\Magento\Captcha\Observer\CaptchaStringResolver::class); + $this->sessionMock = $this->createPartialMock( + \Magento\Framework\Session\SessionManager::class, + ['addErrorMessage'] + ); + $this->dataPersistorMock = $this->getMockBuilder(\Magento\Framework\App\Request\DataPersistorInterface::class) + ->getMockForAbstractClass(); + + $this->checkContactUsFormObserver = $this->objectManagerHelper->getObject( + \Magento\Captcha\Observer\CheckContactUsFormObserver::class, + [ + 'helper' => $this->helperMock, + 'actionFlag' => $this->actionFlagMock, + 'messageManager' => $this->messageManagerMock, + 'redirect' => $this->redirectMock, + 'captchaStringResolver' => $this->captchaStringResolverMock + ] + ); + $this->objectManagerHelper->setBackwardCompatibleProperty( + $this->checkContactUsFormObserver, + 'dataPersistor', + $this->dataPersistorMock + ); + + $this->captchaMock = $this->createMock(\Magento\Captcha\Model\DefaultModel::class); + } + + public function testCheckContactUsFormWhenCaptchaIsRequiredAndValid() + { + $formId = 'contact_us'; + $captchaValue = 'some-value'; + + $controller = $this->createMock(\Magento\Framework\App\Action\Action::class); + $request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $request->expects($this->any()) + ->method('getPost') + ->with(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE, null) + ->willReturn([$formId => $captchaValue]); + $controller->expects($this->any())->method('getRequest')->willReturn($request); + $this->captchaMock->expects($this->any())->method('isRequired')->willReturn(true); + $this->captchaMock->expects($this->once()) + ->method('isCorrect') + ->with($captchaValue) + ->willReturn(true); + $this->captchaStringResolverMock->expects($this->once()) + ->method('resolve') + ->with($request, $formId) + ->willReturn($captchaValue); + $this->helperMock->expects($this->any()) + ->method('getCaptcha') + ->with($formId)->willReturn($this->captchaMock); + $this->sessionMock->expects($this->never())->method('addErrorMessage'); + + $this->checkContactUsFormObserver->execute( + new \Magento\Framework\Event\Observer(['controller_action' => $controller]) + ); + } + + public function testCheckContactUsFormRedirectsCustomerWithWarningMessageWhenCaptchaIsRequiredAndInvalid() + { + $formId = 'contact_us'; + $captchaValue = 'some-value'; + $warningMessage = 'Incorrect CAPTCHA.'; + $redirectRoutePath = 'contact/index/index'; + $redirectUrl = 'http://magento.com/contacts/'; + $postData = ['name' => 'Some Name']; + + $request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $response = $this->createMock(\Magento\Framework\App\Response\Http::class); + $request->expects($this->any()) + ->method('getPost') + ->with(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE, null) + ->willReturn([$formId => $captchaValue]); + $request->expects($this->once()) + ->method('getPostValue') + ->willReturn($postData); + + $this->redirectMock->expects($this->once()) + ->method('redirect') + ->with($response, $redirectRoutePath, []) + ->willReturn($redirectUrl); + + $controller = $this->createMock(\Magento\Framework\App\Action\Action::class); + $controller->expects($this->any())->method('getRequest')->willReturn($request); + $controller->expects($this->any())->method('getResponse')->willReturn($response); + $this->captchaMock->expects($this->any())->method('isRequired')->willReturn(true); + $this->captchaMock->expects($this->once()) + ->method('isCorrect') + ->with($captchaValue) + ->willReturn(false); + $this->captchaStringResolverMock->expects($this->once()) + ->method('resolve') + ->with($request, $formId) + ->willReturn($captchaValue); + $this->helperMock->expects($this->any()) + ->method('getCaptcha') + ->with($formId) + ->willReturn($this->captchaMock); + $this->messageManagerMock->expects($this->once())->method('addErrorMessage')->with($warningMessage); + $this->actionFlagMock->expects($this->once()) + ->method('set') + ->with('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); + $this->dataPersistorMock->expects($this->once()) + ->method('set') + ->with($formId, $postData); + + $this->checkContactUsFormObserver->execute( + new \Magento\Framework\Event\Observer(['controller_action' => $controller]) + ); + } + + public function testCheckContactUsFormDoesNotCheckCaptchaWhenItIsNotRequired() + { + $this->helperMock->expects($this->any()) + ->method('getCaptcha') + ->with('contact_us') + ->willReturn($this->captchaMock); + $this->captchaMock->expects($this->any())->method('isRequired')->willReturn(false); + $this->captchaMock->expects($this->never())->method('isCorrect'); + + $this->checkContactUsFormObserver->execute(new \Magento\Framework\Event\Observer()); + } +} diff --git a/Test/Unit/Observer/CheckForgotpasswordObserverTest.php b/Test/Unit/Observer/CheckForgotpasswordObserverTest.php new file mode 100644 index 0000000..93b5819 --- /dev/null +++ b/Test/Unit/Observer/CheckForgotpasswordObserverTest.php @@ -0,0 +1,156 @@ +_objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->_helper = $this->createMock(\Magento\Captcha\Helper\Data::class); + $this->_actionFlag = $this->createMock(\Magento\Framework\App\ActionFlag::class); + $this->_messageManager = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); + $this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); + $this->captchaStringResolver = $this->createMock(\Magento\Captcha\Observer\CaptchaStringResolver::class); + $this->checkForgotpasswordObserver = $this->_objectManager->getObject( + \Magento\Captcha\Observer\CheckForgotpasswordObserver::class, + [ + 'helper' => $this->_helper, + 'actionFlag' => $this->_actionFlag, + 'messageManager' => $this->_messageManager, + 'redirect' => $this->redirect, + 'captchaStringResolver' => $this->captchaStringResolver + ] + ); + $this->_captcha = $this->createMock(\Magento\Captcha\Model\DefaultModel::class); + } + + public function testCheckForgotpasswordRedirects() + { + $formId = 'user_forgotpassword'; + $captchaValue = 'some-value'; + $warningMessage = 'Incorrect CAPTCHA'; + $redirectRoutePath = '*/*/forgotpassword'; + $redirectUrl = 'http://magento.com/customer/account/forgotpassword/'; + + $request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $response = $this->createMock(\Magento\Framework\App\Response\Http::class); + $request->expects( + $this->any() + )->method( + 'getPost' + )->with( + \Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE, + null + )->will( + $this->returnValue([$formId => $captchaValue]) + ); + + $this->redirect->expects( + $this->once() + )->method( + 'redirect' + )->with( + $response, + $redirectRoutePath, + [] + )->will( + $this->returnValue($redirectUrl) + ); + + $controller = $this->createMock(\Magento\Framework\App\Action\Action::class); + $controller->expects($this->any())->method('getRequest')->will($this->returnValue($request)); + $controller->expects($this->any())->method('getResponse')->will($this->returnValue($response)); + $this->_captcha->expects($this->any())->method('isRequired')->will($this->returnValue(true)); + $this->_captcha->expects( + $this->once() + )->method( + 'isCorrect' + )->with( + $captchaValue + )->will( + $this->returnValue(false) + ); + + $this->captchaStringResolver->expects( + $this->once() + )->method( + 'resolve' + )->with( + $request, + $formId + )->will( + $this->returnValue($captchaValue) + ); + + $this->_helper->expects( + $this->any() + )->method( + 'getCaptcha' + )->with( + $formId + )->will( + $this->returnValue($this->_captcha) + ); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($warningMessage); + $this->_actionFlag->expects( + $this->once() + )->method( + 'set' + )->with( + '', + \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, + true + ); + + $this->checkForgotpasswordObserver->execute( + new \Magento\Framework\Event\Observer(['controller_action' => $controller]) + ); + } +} diff --git a/Test/Unit/Observer/CheckUserCreateObserverTest.php b/Test/Unit/Observer/CheckUserCreateObserverTest.php new file mode 100644 index 0000000..a57faab --- /dev/null +++ b/Test/Unit/Observer/CheckUserCreateObserverTest.php @@ -0,0 +1,169 @@ +_objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->_helper = $this->createMock(\Magento\Captcha\Helper\Data::class); + $this->_actionFlag = $this->createMock(\Magento\Framework\App\ActionFlag::class); + $this->_messageManager = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); + $this->_session = $this->createMock(\Magento\Framework\Session\SessionManager::class); + $this->_urlManager = $this->createMock(\Magento\Framework\Url::class); + $this->captchaStringResolver = $this->createMock(\Magento\Captcha\Observer\CaptchaStringResolver::class); + $this->redirect = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); + $this->checkUserCreateObserver = $this->_objectManager->getObject( + \Magento\Captcha\Observer\CheckUserCreateObserver::class, + [ + 'helper' => $this->_helper, + 'actionFlag' => $this->_actionFlag, + 'messageManager' => $this->_messageManager, + 'session' => $this->_session, + 'urlManager' => $this->_urlManager, + 'redirect' => $this->redirect, + 'captchaStringResolver' => $this->captchaStringResolver + ] + ); + $this->_captcha = $this->createMock(\Magento\Captcha\Model\DefaultModel::class); + } + + public function testCheckUserCreateRedirectsError() + { + $formId = 'user_create'; + $captchaValue = 'some-value'; + $warningMessage = 'Incorrect CAPTCHA'; + $redirectRoutePath = '*/*/create'; + $redirectUrl = 'http://magento.com/customer/account/create/'; + + $request = $this->createMock(\Magento\Framework\App\Request\Http::class); + + $this->redirect->expects( + $this->once() + )->method( + 'error' + )->with( + $redirectUrl + )->will( + $this->returnValue($redirectUrl) + ); + + $response = $this->createMock(\Magento\Framework\App\Response\Http::class); + $response->expects($this->once())->method('setRedirect')->with($redirectUrl); + + $this->_urlManager->expects( + $this->once() + )->method( + 'getUrl' + )->with( + $redirectRoutePath, + ['_nosecret' => true] + )->will( + $this->returnValue($redirectUrl) + ); + + $controller = $this->createMock(\Magento\Framework\App\Action\Action::class); + $controller->expects($this->any())->method('getRequest')->will($this->returnValue($request)); + $controller->expects($this->any())->method('getResponse')->will($this->returnValue($response)); + $this->_captcha->expects($this->any())->method('isRequired')->will($this->returnValue(true)); + $this->_captcha->expects( + $this->once() + )->method( + 'isCorrect' + )->with( + $captchaValue + )->will( + $this->returnValue(false) + ); + $this->captchaStringResolver->expects( + $this->once() + )->method( + 'resolve' + )->with( + $request, + $formId + )->will( + $this->returnValue($captchaValue) + ); + $this->_helper->expects( + $this->any() + )->method( + 'getCaptcha' + )->with( + $formId + )->will( + $this->returnValue($this->_captcha) + ); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($warningMessage); + $this->_actionFlag->expects( + $this->once() + )->method( + 'set' + )->with( + '', + \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, + true + ); + + $this->checkUserCreateObserver->execute( + new \Magento\Framework\Event\Observer(['controller_action' => $controller]) + ); + } +} diff --git a/Test/Unit/Observer/CheckUserEditObserverTest.php b/Test/Unit/Observer/CheckUserEditObserverTest.php new file mode 100644 index 0000000..0f08e5c --- /dev/null +++ b/Test/Unit/Observer/CheckUserEditObserverTest.php @@ -0,0 +1,163 @@ +helperMock = $this->createMock(\Magento\Captcha\Helper\Data::class); + $this->actionFlagMock = $this->createMock(\Magento\Framework\App\ActionFlag::class); + $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); + $this->redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class); + $this->captchaStringResolverMock = $this->createMock(\Magento\Captcha\Observer\CaptchaStringResolver::class); + $this->authenticationMock = $this->getMockBuilder(AuthenticationInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->customerSessionMock = $this->createPartialMock( + \Magento\Customer\Model\Session::class, + ['getCustomerId', 'getCustomer', 'logout', 'start'] + ); + $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->observer = $objectManager->getObject( + \Magento\Captcha\Observer\CheckUserEditObserver::class, + [ + 'helper' => $this->helperMock, + 'actionFlag' => $this->actionFlagMock, + 'messageManager' => $this->messageManagerMock, + 'redirect' => $this->redirectMock, + 'captchaStringResolver' => $this->captchaStringResolverMock, + 'authentication' => $this->authenticationMock, + 'customerSession' => $this->customerSessionMock, + 'scopeConfig' => $this->scopeConfigMock, + ] + ); + } + + /** + * @return void + */ + public function testExecute() + { + $customerId = 7; + $captchaValue = 'some-value'; + $email = 'test@example.com'; + $redirectUrl = 'http://magento.com/customer/account/edit/'; + + $captcha = $this->createMock(\Magento\Captcha\Model\DefaultModel::class); + $captcha->expects($this->once()) + ->method('isRequired') + ->willReturn(true); + $captcha->expects($this->once()) + ->method('isCorrect') + ->with($captchaValue) + ->willReturn(false); + + $this->helperMock->expects($this->once()) + ->method('getCaptcha') + ->with(\Magento\Captcha\Observer\CheckUserEditObserver::FORM_ID) + ->willReturn($captcha); + + $response = $this->createMock(\Magento\Framework\App\Response\Http::class); + $request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $request->expects($this->any()) + ->method('getPost') + ->with(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE, null) + ->willReturn([\Magento\Captcha\Observer\CheckUserEditObserver::FORM_ID => $captchaValue]); + + $controller = $this->createMock(\Magento\Framework\App\Action\Action::class); + $controller->expects($this->any())->method('getRequest')->will($this->returnValue($request)); + $controller->expects($this->any())->method('getResponse')->will($this->returnValue($response)); + + $this->captchaStringResolverMock->expects($this->once()) + ->method('resolve') + ->with($request, \Magento\Captcha\Observer\CheckUserEditObserver::FORM_ID) + ->willReturn($captchaValue); + + $customerDataMock = $this->createMock(\Magento\Customer\Model\Data\Customer::class); + + $this->customerSessionMock->expects($this->once()) + ->method('getCustomerId') + ->willReturn($customerId); + + $this->customerSessionMock->expects($this->atLeastOnce()) + ->method('getCustomer') + ->willReturn($customerDataMock); + + $this->authenticationMock->expects($this->once()) + ->method('processAuthenticationFailure') + ->with($customerId); + $this->authenticationMock->expects($this->once()) + ->method('isLocked') + ->with($customerId) + ->willReturn(true); + + $this->customerSessionMock->expects($this->once()) + ->method('logout'); + $this->customerSessionMock->expects($this->once()) + ->method('start'); + + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->with('contact/email/recipient_email') + ->willReturn($email); + + $message = __('The account is locked. Please wait and try again or contact %1.', $email); + $this->messageManagerMock->expects($this->exactly(2)) + ->method('addErrorMessage') + ->withConsecutive([$message], [__('Incorrect CAPTCHA')]); + + $this->actionFlagMock->expects($this->once()) + ->method('set') + ->with('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); + + $this->redirectMock->expects($this->once()) + ->method('redirect') + ->with($response, '*/*/edit') + ->willReturn($redirectUrl); + + $this->observer->execute(new \Magento\Framework\Event\Observer(['controller_action' => $controller])); + } +} diff --git a/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php b/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php new file mode 100644 index 0000000..415f022 --- /dev/null +++ b/Test/Unit/Observer/CheckUserLoginBackendObserverTest.php @@ -0,0 +1,137 @@ +helperMock = $this->createMock(Data::class); + $this->messageManagerMock = $this->createMock(ManagerInterface::class); + $this->captchaStringResolverMock = $this->createMock(CaptchaStringResolver::class); + $this->requestMock = $this->createMock(RequestInterface::class); + + $this->observer = new CheckUserLoginBackendObserver( + $this->helperMock, + $this->captchaStringResolverMock, + $this->requestMock + ); + } + + /** + * Test check user login in backend with correct captcha + * + * @dataProvider requiredCaptchaDataProvider + * @param bool $isRequired + * @return void + */ + public function testCheckOnBackendLoginWithCorrectCaptcha(bool $isRequired): void + { + $formId = 'backend_login'; + $login = 'admin'; + $captchaValue = 'captcha-value'; + + /** @var Observer|MockObject $observerMock */ + $observerMock = $this->createPartialMock(Observer::class, ['getEvent']); + $eventMock = $this->createPartialMock(Event::class, ['getUsername']); + $captcha = $this->createMock(DefaultModel::class); + + $eventMock->method('getUsername')->willReturn('admin'); + $observerMock->method('getEvent')->willReturn($eventMock); + $captcha->method('isRequired')->with($login)->willReturn($isRequired); + $captcha->method('isCorrect')->with($captchaValue)->willReturn(true); + $this->helperMock->method('getCaptcha')->with($formId)->willReturn($captcha); + $this->captchaStringResolverMock->method('resolve')->with($this->requestMock, $formId) + ->willReturn($captchaValue); + + $this->observer->execute($observerMock); + } + + /** + * @return array + */ + public function requiredCaptchaDataProvider(): array + { + return [ + [true], + [false] + ]; + } + + /** + * Test check user login in backend with wrong captcha + * + * @return void + * @expectedException \Magento\Framework\Exception\Plugin\AuthenticationException + */ + public function testCheckOnBackendLoginWithWrongCaptcha(): void + { + $formId = 'backend_login'; + $login = 'admin'; + $captchaValue = 'captcha-value'; + + /** @var Observer|MockObject $observerMock */ + $observerMock = $this->createPartialMock(Observer::class, ['getEvent']); + $eventMock = $this->createPartialMock(Event::class, ['getUsername']); + $captcha = $this->createMock(DefaultModel::class); + + $eventMock->method('getUsername')->willReturn($login); + $observerMock->method('getEvent')->willReturn($eventMock); + $captcha->method('isRequired')->with($login)->willReturn(true); + $captcha->method('isCorrect')->with($captchaValue)->willReturn(false); + $this->helperMock->method('getCaptcha')->with($formId)->willReturn($captcha); + $this->captchaStringResolverMock->method('resolve')->with($this->requestMock, $formId) + ->willReturn($captchaValue); + + $this->observer->execute($observerMock); + } +} diff --git a/Test/Unit/Observer/CheckUserLoginObserverTest.php b/Test/Unit/Observer/CheckUserLoginObserverTest.php new file mode 100644 index 0000000..0499ec3 --- /dev/null +++ b/Test/Unit/Observer/CheckUserLoginObserverTest.php @@ -0,0 +1,169 @@ +helperMock = $this->createMock(\Magento\Captcha\Helper\Data::class); + $this->actionFlagMock = $this->createMock(\Magento\Framework\App\ActionFlag::class); + $this->messageManagerMock = $this->createMock(\Magento\Framework\Message\ManagerInterface::class); + $this->customerSessionMock = $this->createPartialMock( + \Magento\Customer\Model\Session::class, + ['setUsername', 'getBeforeAuthUrl'] + ); + $this->captchaStringResolverMock = $this->createMock(\Magento\Captcha\Observer\CaptchaStringResolver::class); + $this->customerUrlMock = $this->createMock(\Magento\Customer\Model\Url::class); + $this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class); + $this->authenticationMock = $this->createMock(AuthenticationInterface::class); + + $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->observer = $objectManager->getObject( + \Magento\Captcha\Observer\CheckUserLoginObserver::class, + [ + 'helper' => $this->helperMock, + 'actionFlag' => $this->actionFlagMock, + 'messageManager' => $this->messageManagerMock, + 'customerSession' => $this->customerSessionMock, + 'captchaStringResolver' => $this->captchaStringResolverMock, + 'customerUrl' => $this->customerUrlMock, + ] + ); + + $reflection = new \ReflectionClass(get_class($this->observer)); + $reflectionProperty = $reflection->getProperty('authentication'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->observer, $this->authenticationMock); + + $reflectionProperty2 = $reflection->getProperty('customerRepository'); + $reflectionProperty2->setAccessible(true); + $reflectionProperty2->setValue($this->observer, $this->customerRepositoryMock); + } + + /** + * @return void + */ + public function testExecute() + { + $formId = 'user_login'; + $login = 'login'; + $loginParams = ['username' => $login]; + $customerId = 7; + $redirectUrl = 'http://magento.com/customer/account/login/'; + $captchaValue = 'some-value'; + + $captcha = $this->createMock(\Magento\Captcha\Model\DefaultModel::class); + $captcha->expects($this->once()) + ->method('isRequired') + ->with($login) + ->willReturn(true); + $captcha->expects($this->once()) + ->method('isCorrect') + ->with($captchaValue) + ->willReturn(false); + $captcha->expects($this->once()) + ->method('logAttempt') + ->with($login); + + $this->helperMock->expects($this->once()) + ->method('getCaptcha') + ->with($formId) + ->willReturn($captcha); + + $response = $this->createMock(\Magento\Framework\App\Response\Http::class); + $response->expects($this->once()) + ->method('setRedirect') + ->with($redirectUrl); + + $request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $request->expects($this->any()) + ->method('getPost') + ->with('login') + ->willReturn($loginParams); + + $controller = $this->createMock(\Magento\Framework\App\Action\Action::class); + $controller->expects($this->any())->method('getRequest')->will($this->returnValue($request)); + $controller->expects($this->any())->method('getResponse')->will($this->returnValue($response)); + + $this->captchaStringResolverMock->expects($this->once()) + ->method('resolve') + ->with($request, $formId) + ->willReturn($captchaValue); + + $customerDataMock = $this->createPartialMock(\Magento\Customer\Model\Data\Customer::class, ['getId']); + $customerDataMock->expects($this->once()) + ->method('getId') + ->willReturn($customerId); + + $this->customerRepositoryMock->expects($this->once()) + ->method('get') + ->with($login) + ->willReturn($customerDataMock); + + $this->authenticationMock->expects($this->once()) + ->method('processAuthenticationFailure') + ->with($customerId); + + $this->messageManagerMock->expects($this->once()) + ->method('addErrorMessage') + ->with(__('Incorrect CAPTCHA')); + + $this->actionFlagMock->expects($this->once()) + ->method('set') + ->with('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true); + + $this->customerSessionMock->expects($this->once()) + ->method('setUsername') + ->with($login); + + $this->customerSessionMock->expects($this->once()) + ->method('getBeforeAuthUrl') + ->willReturn(false); + + $this->customerUrlMock->expects($this->once()) + ->method('getLoginUrl') + ->willReturn($redirectUrl); + + $this->observer->execute(new \Magento\Framework\Event\Observer(['controller_action' => $controller])); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f151b94 --- /dev/null +++ b/composer.json @@ -0,0 +1,35 @@ +{ + "name": "magenxcommerce/module-captcha", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magenxcommerce/framework": "102.0.*", + "magenxcommerce/module-backend": "101.0.*", + "magenxcommerce/module-checkout": "100.3.*", + "magenxcommerce/module-customer": "102.0.*", + "magenxcommerce/module-store": "101.0.*", + "laminas/laminas-captcha": "^2.7.1", + "laminas/laminas-db": "^2.8.2", + "laminas/laminas-session": "^2.7.3" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\Captcha\\": "" + } + }, + "version": "100.3.5", + "replace": { + "magento/module-captcha": "*" + } +} \ No newline at end of file diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml new file mode 100644 index 0000000..139bf9e --- /dev/null +++ b/etc/adminhtml/di.xml @@ -0,0 +1,31 @@ + + + + + + + Magento\Backend\Model\Auth\Session + Magento\Captcha\Helper\Adminhtml\Data + + + + + Magento\Backend\Model\Session + + + + + Magento\Backend\Model\Session + + + + + Magento\Backend\Model\Session + + + diff --git a/etc/adminhtml/events.xml b/etc/adminhtml/events.xml new file mode 100644 index 0000000..e9de9bc --- /dev/null +++ b/etc/adminhtml/events.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/etc/adminhtml/routes.xml b/etc/adminhtml/routes.xml new file mode 100644 index 0000000..4d48e63 --- /dev/null +++ b/etc/adminhtml/routes.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml new file mode 100644 index 0000000..a054309 --- /dev/null +++ b/etc/adminhtml/system.xml @@ -0,0 +1,154 @@ + + + + + + + CAPTCHA + + Enable CAPTCHA in Admin + Magento\Config\Model\Config\Source\Yesno + + + Font + Magento\Captcha\Model\Config\Font + + 1 + + + + Forms + Magento\Captcha\Model\Config\Form\Backend + + 1 + + + + Displaying Mode + Magento\Captcha\Model\Config\Mode + + 1 + + + + Number of Unsuccessful Attempts to Login + If 0 is specified, CAPTCHA on the Login form will be always available. + + after_fail + 1 + + required-entry validate-digits + + + CAPTCHA Timeout (minutes) + + 1 + + required-entry validate-digits + + + Number of Symbols + Please specify 8 symbols at the most. Range allowed (e.g. 3-5) + + 1 + + required-entry + + + Symbols Used in CAPTCHA + + Similar looking characters (e.g. "i", "l", "1") decrease chance of correct recognition by customer.]]> + + + 1 + + required-entry validate-alphanum + + + Case Sensitive + Magento\Config\Model\Config\Source\Yesno + + 1 + + + + + + + CAPTCHA + + Enable CAPTCHA on Storefront + Magento\Config\Model\Config\Source\Yesno + + + Font + Magento\Captcha\Model\Config\Font + + 1 + + + + Forms + Magento\Captcha\Model\Config\Form\Frontend + CAPTCHA for "Create user" and "Forgot password" forms is always enabled if chosen. + + 1 + + + + Displaying Mode + Magento\Captcha\Model\Config\Mode + + 1 + + + + Number of Unsuccessful Attempts to Login + If 0 is specified, CAPTCHA on the Login form will be always available. + + 1 + after_fail + + required-entry validate-digits + + + CAPTCHA Timeout (minutes) + + 1 + + required-entry validate-digits + + + Number of Symbols + Please specify 8 symbols at the most. Range allowed (e.g. 3-5) + + 1 + + required-entry validate-range range-1-8 + + + Symbols Used in CAPTCHA + + Similar looking characters (e.g. "i", "l", "1") decrease chance of correct recognition by customer.]]> + + + 1 + + required-entry validate-alphanum + + + Case Sensitive + Magento\Config\Model\Config\Source\Yesno + + 1 + + + + + + diff --git a/etc/config.xml b/etc/config.xml new file mode 100644 index 0000000..dd748dd --- /dev/null +++ b/etc/config.xml @@ -0,0 +1,98 @@ + + + + + + + + captcha + + + + + + default + 1 + linlibertine + after_fail + backend_forgotpassword,backend_login + 3 + 1000 + 7 + 4-5 + ABCDEFGHJKMnpqrstuvwxyz23456789 + 0 + + + 1 + + + + + + default + 1 + linlibertine + after_fail + user_forgotpassword,user_login + 3 + 1000 + 7 + 4-5 + ABCDEFGHJKMnpqrstuvwxyz23456789 + 0 + + 1 + 1 + + + 1 + 1 + 1 + + + + + + + LinLibertine + LinLibertineFont/LinLibertine_Bd-2.8.1.ttf + + + + + + Create user + + + Login + + + Forgot password + + + Contact Us + + + Change password + + + + + + + Admin Login + + + Admin Forgot Password + + + + + + diff --git a/etc/crontab.xml b/etc/crontab.xml new file mode 100644 index 0000000..09909ff --- /dev/null +++ b/etc/crontab.xml @@ -0,0 +1,17 @@ + + + + + + */30 * * * * + + + */10 * * * * + + + diff --git a/etc/crontab/di.xml b/etc/crontab/di.xml new file mode 100644 index 0000000..4a86160 --- /dev/null +++ b/etc/crontab/di.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/etc/db_schema.xml b/etc/db_schema.xml new file mode 100644 index 0000000..158e2f4 --- /dev/null +++ b/etc/db_schema.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/etc/db_schema_whitelist.json b/etc/db_schema_whitelist.json new file mode 100644 index 0000000..1f5b1b6 --- /dev/null +++ b/etc/db_schema_whitelist.json @@ -0,0 +1,13 @@ +{ + "captcha_log": { + "column": { + "type": true, + "value": true, + "count": true, + "updated_at": true + }, + "constraint": { + "PRIMARY": true + } + } +} \ No newline at end of file diff --git a/etc/di.xml b/etc/di.xml new file mode 100644 index 0000000..83c4e8a --- /dev/null +++ b/etc/di.xml @@ -0,0 +1,42 @@ + + + + + + Magento\Customer\Model\Session + + + + + Magento\Customer\Model\Session + + + + + Magento\Customer\Model\Session + + + + + Magento\Customer\Model\Session + + + + + + + + + user_login + + + + + + + diff --git a/etc/events.xml b/etc/events.xml new file mode 100644 index 0000000..970c0d0 --- /dev/null +++ b/etc/events.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml new file mode 100644 index 0000000..490f1ea --- /dev/null +++ b/etc/frontend/di.xml @@ -0,0 +1,37 @@ + + + + + + + Magento\Captcha\Model\Checkout\ConfigProvider + + + + + + + user_login + + + + + + + user_login + + + + + + + Magento\Captcha\CustomerData\Captcha + + + + diff --git a/etc/frontend/events.xml b/etc/frontend/events.xml new file mode 100644 index 0000000..2f318d3 --- /dev/null +++ b/etc/frontend/events.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/etc/frontend/routes.xml b/etc/frontend/routes.xml new file mode 100644 index 0000000..895f18f --- /dev/null +++ b/etc/frontend/routes.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/etc/frontend/sections.xml b/etc/frontend/sections.xml new file mode 100644 index 0000000..7f2070e --- /dev/null +++ b/etc/frontend/sections.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/etc/module.xml b/etc/module.xml new file mode 100644 index 0000000..36a44a6 --- /dev/null +++ b/etc/module.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/i18n/en_US.csv b/i18n/en_US.csv new file mode 100644 index 0000000..480107d --- /dev/null +++ b/i18n/en_US.csv @@ -0,0 +1,26 @@ +Always,Always +"After number of attempts to login","After number of attempts to login" +"Provided form does not exist","Provided form does not exist" +"Incorrect CAPTCHA","Incorrect CAPTCHA" +"Incorrect CAPTCHA.","Incorrect CAPTCHA." +"The account is locked. Please wait and try again or contact %1.","The account is locked. Please wait and try again or contact %1." +"Please enter the letters and numbers from the image","Please enter the letters and numbers from the image" +"Attention: Captcha is case sensitive.","Attention: Captcha is case sensitive." +"Reload captcha","Reload captcha" +"Please type the letters and numbers below","Please type the letters and numbers below" +"Attention: Captcha is case sensitive.","Attention: Captcha is case sensitive." +CAPTCHA,CAPTCHA +"Enable CAPTCHA in Admin","Enable CAPTCHA in Admin" +Font,Font +Forms,Forms +"Displaying Mode","Displaying Mode" +"Number of Unsuccessful Attempts to Login","Number of Unsuccessful Attempts to Login" +"If 0 is specified, CAPTCHA on the Login form will be always available.","If 0 is specified, CAPTCHA on the Login form will be always available." +"CAPTCHA Timeout (minutes)","CAPTCHA Timeout (minutes)" +"Number of Symbols","Number of Symbols" +"Please specify 8 symbols at the most. Range allowed (e.g. 3-5)","Please specify 8 symbols at the most. Range allowed (e.g. 3-5)" +"Symbols Used in CAPTCHA","Symbols Used in CAPTCHA" +"Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer.","Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.Similar looking characters (e.g. ""i"", ""l"", ""1"") decrease chance of correct recognition by customer." +"Case Sensitive","Case Sensitive" +"Enable CAPTCHA on Storefront","Enable CAPTCHA on Storefront" +"CAPTCHA for ""Create user"" and ""Forgot password"" forms is always enabled if chosen.","CAPTCHA for ""Create user"" and ""Forgot password"" forms is always enabled if chosen." diff --git a/registration.php b/registration.php new file mode 100644 index 0000000..d6c49c7 --- /dev/null +++ b/registration.php @@ -0,0 +1,9 @@ + + + + + + + + backend_forgotpassword + + + 226 + + + 50 + + + + + diff --git a/view/adminhtml/layout/adminhtml_auth_login.xml b/view/adminhtml/layout/adminhtml_auth_login.xml new file mode 100644 index 0000000..f9ac960 --- /dev/null +++ b/view/adminhtml/layout/adminhtml_auth_login.xml @@ -0,0 +1,24 @@ + + + + + + + + backend_login + + + 226 + + + 50 + + + + + diff --git a/view/adminhtml/templates/default.phtml b/view/adminhtml/templates/default.phtml new file mode 100644 index 0000000..88e0d5e --- /dev/null +++ b/view/adminhtml/templates/default.phtml @@ -0,0 +1,55 @@ +getCaptchaModel(); +?> + + + = $block->escapeHtml(__('Please enter the letters and numbers from the image')) ?> + + + + isCaseSensitive()) :?> + + = $block->escapeHtml(__('Attention: Captcha is case sensitive.'), ['strong']) ?> + + + + + + + + + diff --git a/view/adminhtml/web/reload.png b/view/adminhtml/web/reload.png new file mode 100644 index 0000000..68d4bff Binary files /dev/null and b/view/adminhtml/web/reload.png differ diff --git a/view/frontend/layout/checkout_index_index.xml b/view/frontend/layout/checkout_index_index.xml new file mode 100644 index 0000000..7180372 --- /dev/null +++ b/view/frontend/layout/checkout_index_index.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + Magento_Captcha/js/view/checkout/loginCaptcha + additional-login-form-fields + user_login + checkoutConfig + + + + + + + + + + + + + + + Magento_Captcha/js/view/checkout/loginCaptcha + additional-login-form-fields + user_login + checkoutConfig + + + + + + + + + + + + + + + + + + + diff --git a/view/frontend/layout/contact_index_index.xml b/view/frontend/layout/contact_index_index.xml new file mode 100644 index 0000000..df6de5b --- /dev/null +++ b/view/frontend/layout/contact_index_index.xml @@ -0,0 +1,27 @@ + + + + + + + + contact_us + + + 230 + + + 50 + + + + + + + + diff --git a/view/frontend/layout/customer_account_create.xml b/view/frontend/layout/customer_account_create.xml new file mode 100644 index 0000000..1ba5deb --- /dev/null +++ b/view/frontend/layout/customer_account_create.xml @@ -0,0 +1,27 @@ + + + + + + + + user_create + + + 230 + + + 50 + + + + + + + + diff --git a/view/frontend/layout/customer_account_edit.xml b/view/frontend/layout/customer_account_edit.xml new file mode 100644 index 0000000..826e288 --- /dev/null +++ b/view/frontend/layout/customer_account_edit.xml @@ -0,0 +1,27 @@ + + + + + + + + user_edit + + + 230 + + + 50 + + + + + + + + diff --git a/view/frontend/layout/customer_account_forgotpassword.xml b/view/frontend/layout/customer_account_forgotpassword.xml new file mode 100644 index 0000000..cbaabf6 --- /dev/null +++ b/view/frontend/layout/customer_account_forgotpassword.xml @@ -0,0 +1,27 @@ + + + + + + + + user_forgotpassword + + + 230 + + + 50 + + + + + + + + diff --git a/view/frontend/layout/customer_account_login.xml b/view/frontend/layout/customer_account_login.xml new file mode 100644 index 0000000..50d99cf --- /dev/null +++ b/view/frontend/layout/customer_account_login.xml @@ -0,0 +1,27 @@ + + + + + + + + user_login + + + 230 + + + 50 + + + + + + + + diff --git a/view/frontend/layout/default.xml b/view/frontend/layout/default.xml new file mode 100644 index 0000000..5015d6b --- /dev/null +++ b/view/frontend/layout/default.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + Magento_Captcha/js/view/checkout/loginCaptcha + additional-login-form-fields + user_login + checkout + + + + + + + + + diff --git a/view/frontend/requirejs-config.js b/view/frontend/requirejs-config.js new file mode 100644 index 0000000..42c8063 --- /dev/null +++ b/view/frontend/requirejs-config.js @@ -0,0 +1,13 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + map: { + '*': { + captcha: 'Magento_Captcha/js/captcha', + 'Magento_Captcha/captcha': 'Magento_Captcha/js/captcha' + } + } +}; diff --git a/view/frontend/templates/default.phtml b/view/frontend/templates/default.phtml new file mode 100644 index 0000000..ead8c59 --- /dev/null +++ b/view/frontend/templates/default.phtml @@ -0,0 +1,35 @@ +getCaptchaModel(); +?> + + = $block->escapeHtml(__('Please type the letters and numbers below')) ?> + + + + ", + "type": "= $block->escapeHtmlAttr($block->getFormId()) ?>"}}'> + + + = $block->escapeHtml(__('Reload captcha')) ?> + + + isCaseSensitive()) :?> + + = $block->escapeHtml(__('Attention: Captcha is case sensitive.'), ['strong']) ?> + + + + + diff --git a/view/frontend/templates/js/components.phtml b/view/frontend/templates/js/components.phtml new file mode 100644 index 0000000..5902a9f --- /dev/null +++ b/view/frontend/templates/js/components.phtml @@ -0,0 +1,7 @@ + += $block->getChildHtml() ?> diff --git a/view/frontend/web/js/action/refresh.js b/view/frontend/web/js/action/refresh.js new file mode 100644 index 0000000..c822782 --- /dev/null +++ b/view/frontend/web/js/action/refresh.js @@ -0,0 +1,26 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'mage/storage' +], function (storage) { + 'use strict'; + + return function (refreshUrl, formId, imageSource) { + return storage.post( + refreshUrl, + JSON.stringify({ + 'formId': formId + }), + false + ).done( + function (response) { + if (response.imgSrc) { + imageSource(response.imgSrc); + } + } + ); + }; +}); diff --git a/view/frontend/web/js/captcha.js b/view/frontend/web/js/captcha.js new file mode 100644 index 0000000..b5e5e6b --- /dev/null +++ b/view/frontend/web/js/captcha.js @@ -0,0 +1,70 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery-ui-modules/widget' +], function ($) { + 'use strict'; + + /** + * @api + */ + $.widget('mage.captcha', { + options: { + refreshClass: 'refreshing', + reloadSelector: '.captcha-reload', + imageSelector: '.captcha-img', + imageLoader: '' + }, + + /** + * Method binds click event to reload image + * @private + */ + _create: function () { + this.element.on('click', this.options.reloadSelector, $.proxy(this.refresh, this)); + }, + + /** + * Method triggers an AJAX request to refresh the CAPTCHA image + */ + refresh: function () { + var imageLoader = this.options.imageLoader; + + if (imageLoader) { + this.element.find(this.options.imageSelector).attr('src', imageLoader); + } + this.element.addClass(this.options.refreshClass); + + $.ajax({ + url: this.options.url, + type: 'post', + async: false, + dataType: 'json', + context: this, + data: { + 'formId': this.options.type + }, + + /** + * @param {Object} response + */ + success: function (response) { + if (response.imgSrc) { + this.element.find(this.options.imageSelector).attr('src', response.imgSrc); + } + }, + + /** Complete callback. */ + complete: function () { + this.element.removeClass(this.options.refreshClass); + } + }); + } + }); + + return $.mage.captcha; +}); diff --git a/view/frontend/web/js/model/captcha.js b/view/frontend/web/js/model/captcha.js new file mode 100644 index 0000000..e79cfb3 --- /dev/null +++ b/view/frontend/web/js/model/captcha.js @@ -0,0 +1,155 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/*global alert*/ +define([ + 'jquery', + 'ko', + 'Magento_Captcha/js/action/refresh' +], function ($, ko, refreshAction) { + 'use strict'; + + return function (captchaData) { + return { + formId: captchaData.formId, + imageSource: ko.observable(captchaData.imageSrc), + visibility: ko.observable(false), + captchaValue: ko.observable(null), + isRequired: ko.observable(captchaData.isRequired), + isCaseSensitive: captchaData.isCaseSensitive, + imageHeight: captchaData.imageHeight, + refreshUrl: captchaData.refreshUrl, + isLoading: ko.observable(false), + timestamp: null, + + /** + * @return {String} + */ + getFormId: function () { + return this.formId; + }, + + /** + * @param {String} formId + */ + setFormId: function (formId) { + this.formId = formId; + }, + + /** + * @return {Boolean} + */ + getIsVisible: function () { + return this.visibility(); + }, + + /** + * @param {Boolean} flag + */ + setIsVisible: function (flag) { + this.visibility(flag); + }, + + /** + * @return {Boolean} + */ + getIsRequired: function () { + return this.isRequired(); + }, + + /** + * @param {Boolean} flag + */ + setIsRequired: function (flag) { + this.isRequired(flag); + }, + + /** + * @return {Boolean} + */ + getIsCaseSensitive: function () { + return this.isCaseSensitive; + }, + + /** + * @param {Boolean} flag + */ + setIsCaseSensitive: function (flag) { + this.isCaseSensitive = flag; + }, + + /** + * @return {String|Number} + */ + getImageHeight: function () { + return this.imageHeight; + }, + + /** + * @param {String|Number}height + */ + setImageHeight: function (height) { + this.imageHeight = height; + }, + + /** + * @return {String} + */ + getImageSource: function () { + return this.imageSource; + }, + + /** + * @param {String} imageSource + */ + setImageSource: function (imageSource) { + this.imageSource(imageSource); + }, + + /** + * @return {String} + */ + getRefreshUrl: function () { + return this.refreshUrl; + }, + + /** + * @param {String} url + */ + setRefreshUrl: function (url) { + this.refreshUrl = url; + }, + + /** + * @return {*} + */ + getCaptchaValue: function () { + return this.captchaValue; + }, + + /** + * @param {*} value + */ + setCaptchaValue: function (value) { + this.captchaValue(value); + }, + + /** + * Refresh captcha. + */ + refresh: function () { + var refresh, + self = this; + + this.isLoading(true); + + refresh = refreshAction(this.getRefreshUrl(), this.getFormId(), this.getImageSource()); + $.when(refresh).done(function () { + self.isLoading(false); + }); + } + }; + }; +}); diff --git a/view/frontend/web/js/model/captchaList.js b/view/frontend/web/js/model/captchaList.js new file mode 100644 index 0000000..43f29f2 --- /dev/null +++ b/view/frontend/web/js/model/captchaList.js @@ -0,0 +1,44 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define(['jquery'], function ($) { + 'use strict'; + + var captchaList = []; + + return { + /** + * @param {Object} captcha + */ + add: function (captcha) { + captchaList.push(captcha); + }, + + /** + * @param {String} formId + * @return {Object} + */ + getCaptchaByFormId: function (formId) { + var captcha = null; + + $.each(captchaList, function (key, item) { + if (formId === item.formId) { + captcha = item; + + return false; + } + }); + + return captcha; + }, + + /** + * @return {Array} + */ + getCaptchaList: function () { + return captchaList; + } + }; +}); diff --git a/view/frontend/web/js/view/checkout/defaultCaptcha.js b/view/frontend/web/js/view/checkout/defaultCaptcha.js new file mode 100644 index 0000000..d79c42a --- /dev/null +++ b/view/frontend/web/js/view/checkout/defaultCaptcha.js @@ -0,0 +1,168 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'Magento_Captcha/js/model/captcha', + 'Magento_Captcha/js/model/captchaList', + 'Magento_Customer/js/customer-data', + 'underscore' +], function ($, Component, Captcha, captchaList, customerData, _) { + 'use strict'; + + var captchaConfig; + + return Component.extend({ + defaults: { + template: 'Magento_Captcha/checkout/captcha' + }, + dataScope: 'global', + currentCaptcha: null, + + /** + * @return {*} + */ + captchaValue: function () { + return this.currentCaptcha.getCaptchaValue(); + }, + + /** @inheritdoc */ + initialize: function () { + this._super(); + + if (window[this.configSource] && window[this.configSource].captcha) { + captchaConfig = window[this.configSource].captcha; + $.each(captchaConfig, function (formId, captchaData) { + var captcha; + + captchaData.formId = formId; + captcha = Captcha(captchaData); + this.checkCustomerData(formId, customerData.get('captcha')(), captcha); + this.subscribeCustomerData(formId, captcha); + captchaList.add(captcha); + }.bind(this)); + } + }, + + /** + * Check customer data for captcha configuration. + * + * @param {String} formId + * @param {Object} captchaData + * @param {Object} captcha + */ + checkCustomerData: function (formId, captchaData, captcha) { + if (!_.isEmpty(captchaData) && + !_.isEmpty(captchaData)[formId] && + captchaData[formId].timestamp > captcha.timestamp + ) { + if (!captcha.isRequired() && captchaData[formId].isRequired) { + captcha.refresh(); + } + captcha.isRequired(captchaData[formId].isRequired); + captcha.timestamp = captchaData[formId].timestamp; + } + }, + + /** + * Subscribe for customer data updates. + * + * @param {String} formId + * @param {Object} captcha + */ + subscribeCustomerData: function (formId, captcha) { + customerData.get('captcha').subscribe(function (captchaData) { + this.checkCustomerData(formId, captchaData, captcha); + }.bind(this)); + }, + + /** + * @return {Boolean} + */ + getIsLoading: function () { + return this.currentCaptcha !== null ? this.currentCaptcha.isLoading : false; + }, + + /** + * @return {null|Object} + */ + getCurrentCaptcha: function () { + return this.currentCaptcha; + }, + + /** + * @param {Object} captcha + */ + setCurrentCaptcha: function (captcha) { + this.currentCaptcha = captcha; + }, + + /** + * @return {String|null} + */ + getFormId: function () { + return this.currentCaptcha !== null ? this.currentCaptcha.getFormId() : null; + }, + + /** + * @return {Boolean} + */ + getIsVisible: function () { + return this.currentCaptcha !== null ? this.currentCaptcha.getIsVisible() : false; + }, + + /** + * @param {Boolean} flag + */ + setIsVisible: function (flag) { + this.currentCaptcha.setIsVisible(flag); + }, + + /** + * @return {Boolean} + */ + isRequired: function () { + return this.currentCaptcha !== null ? this.currentCaptcha.getIsRequired() : false; + }, + + /** + * Set isRequired on current captcha model. + * + * @param {Boolean} flag + */ + setIsRequired: function (flag) { + this.currentCaptcha.setIsRequired(flag); + }, + + /** + * @return {Boolean} + */ + isCaseSensitive: function () { + return this.currentCaptcha !== null ? this.currentCaptcha.getIsCaseSensitive() : false; + }, + + /** + * @return {String|Number|null} + */ + imageHeight: function () { + return this.currentCaptcha !== null ? this.currentCaptcha.getImageHeight() : null; + }, + + /** + * @return {String|null} + */ + getImageSource: function () { + return this.currentCaptcha !== null ? this.currentCaptcha.getImageSource() : null; + }, + + /** + * Refresh captcha. + */ + refresh: function () { + this.currentCaptcha.refresh(); + } + }); +}); diff --git a/view/frontend/web/js/view/checkout/loginCaptcha.js b/view/frontend/web/js/view/checkout/loginCaptcha.js new file mode 100644 index 0000000..49528f6 --- /dev/null +++ b/view/frontend/web/js/view/checkout/loginCaptcha.js @@ -0,0 +1,39 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Captcha/js/view/checkout/defaultCaptcha', + 'Magento_Captcha/js/model/captchaList', + 'Magento_Customer/js/action/login', + 'underscore' +], +function (defaultCaptcha, captchaList, loginAction, _) { + 'use strict'; + + return defaultCaptcha.extend({ + /** @inheritdoc */ + initialize: function () { + var self = this, + currentCaptcha; + + this._super(); + currentCaptcha = captchaList.getCaptchaByFormId(this.formId); + + if (currentCaptcha != null) { + currentCaptcha.setIsVisible(true); + this.setCurrentCaptcha(currentCaptcha); + + loginAction.registerLoginCallback(function (loginData) { + if (loginData['captcha_form_id'] && + loginData['captcha_form_id'] === self.formId && + self.isRequired() + ) { + _.defer(self.refresh.bind(self)); + } + }); + } + } + }); +}); diff --git a/view/frontend/web/template/checkout/captcha.html b/view/frontend/web/template/checkout/captcha.html new file mode 100644 index 0000000..3f48ec3 --- /dev/null +++ b/view/frontend/web/template/checkout/captcha.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + +